Java的迭代器與迭代子模式

概括

Java集合容器是Java的一個重要組成部分,而迭代器(Iterator)就是對外提供訪問集合元素的一種方式。

訪問數組元素的方式

訪問數組的方式並不陌生,如下

public class Test {

    public static void main(String[] args) {
        int[] arr = {2, 0, 1, 8, 0, 6, 0, 7};
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}

或者也可以使用語法糖的寫法

public class Test {

    public static void main(String[] args) {
        int[] arr = {2, 0, 1, 8, 0, 6, 0, 7};
        for (int a : arr) {
            System.out.println(a);
        }
    }
}

對於後者,如果查看.class反編譯的文件可以發現如下代碼

public class Test {
    public Test() {
    }

    public static void main(String[] args) throws IOException {
        int[] arr = new int[]{2, 0, 1, 8, 0, 6, 0, 7};
        int[] var2 = arr;
        int var3 = arr.length;

        for(int var4 = 0; var4 < var3; ++var4) {
            int a = var2[var4];
            System.out.println(a);
        }

    }
}

不難發現,反編譯出來的代碼實際上就是傳統的下標訪問方式

語法糖作用於數組或者繼承了Iterable<T>的接口上,數組已經驗證了,接下來驗證一下後者,拿最常用的ArrayList來舉示例

public class Test {

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(2, 0, 1, 8, 0, 6, 0, 7);
        for (Integer i : list) {
            System.out.println(i);
        }
    }
}

反編譯後的代碼

public class Test {
    public Test() {
    }

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(2, 0, 1, 8, 0, 6, 0, 7);
        Iterator var2 = list.iterator();

        while(var2.hasNext()) {
            Integer i = (Integer)var2.next();
            System.out.println(i);
        }

    }
}

從代碼中可以看出,編譯後語法糖被轉化成了迭代器(Iterator)的訪問模式,還順便驗證了泛型擦除

還以一種類似於傳統數組的訪問方式,反編譯後基本沒有變化

public class Test {

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(2, 0, 1, 8, 0, 6, 0, 7);
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
}

到這裏就會問,兩種訪問方式的效率怎麼樣?

來看一下測試代碼

public class Test {

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();

        // 向list中裝入50000個隨機數
        putRandomNumbers(list, 50000);

        long start, end;

        /*
         * fori下標訪問
         */
        start = System.currentTimeMillis();
        for (int i = 0; i < list.size(); i++) {
            list.get(i);
        }
        end = System.currentTimeMillis();
        System.out.println("fori: " + (end - start) + "ms");

        /*
         * foreach(迭代器)訪問
         */
        start = System.currentTimeMillis();
        for (Integer i : list) {

        }
        end = System.currentTimeMillis();
        System.out.println("foreach: " + (end - start) + "ms");
    }


    public static void putRandomNumbers(List<Integer> list, int length) {
        Random r = new Random();
        for (int i = 0; i < length; i++) {
            list.add(r.nextInt());
        }
    }
}

運行結果是

——————

fori: 5ms

foreach: 6ms

——————

如果數組大小改爲500000

——————

fori: 14ms

foreach: 20ms

——————

一次測試可能不怎麼可靠,我測了很多次fori都是在10~20ms,而foreach在15ms~25ms上,暫且認爲fori比foreach快吧

那麼問題來了,既然傳統的fori比foreach(迭代器)快,還要迭代器來幹什麼?

現在改一下上面的代碼,就把ArrayList改成LinkedList,其他不變,再看看運行結果

(ps:數組長度爲50000)

——————

fori: 3817ms

foreach: 10ms

——————

差距一下就出來了,下面是從源碼來分析爲什麼兩種list的fori效率差距爲何如此之大

對於ArrayList,get方法的源碼如下

先對下標做檢查(防止越界)

    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

再跳到elementData()中

    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }

明顯這個get方法就是直接訪問數組的下標,訪問數組單個元素,時間複雜度O(1)


再來看看LinkedList

    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

跳轉到node方法

    Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

可見LinkedList內部是一個雙向鏈表,當訪問某一個元素時,先檢查下標在鏈表的前半段還是後半段,如果在前半段,從前往後依次訪問,否則從後往前依次訪問,時間複雜度O(n/2),姑且也算作O(n)吧

這樣一來,用fori的方式遍歷List,調用n次get方法,對於ArrayList的時間複雜度就是O(n),而LinkedList就變成了O(n^2)!所以會導致fori訪問上ArrayList和LinkedList的速度差距如此之大。

迭代器與迭代子模式

遍歷一個數組和遍歷一個鏈表的方式是不一樣的,而在Java中,集合不只是有ArrayList和LinkedList,還有各種Set和Map,我們希望能夠有統一的方式來遍歷一個集合。迭代器恰好爲我們解決了這個問題

迭代器的引入使得在遍歷集合時,客戶端(使用集合的一端)可以不必關心集合是以怎樣的方式遍歷的,因爲集合對外提供了統一的獲取迭代器接口,當集合內部結構需要做修改時,也要修改對應的迭代器接口的邏輯,但是客戶端的遍歷邏輯是不需要變動的,這樣一來保證了開-閉原則,這也就是所說的迭代子模式


迭代子模式定義

在軟件構建過程中,集合對象內部結構常常變化各異,但對於這些集合對象,我們希望在不暴露其內部結構的同時,可以讓外部客戶代碼透明地訪問其中包含的元素;同時這種“透明遍歷”也爲同一種算法在多種集合對象上進行操作提供了可能。使用面向對象技術將這種遍歷機制抽象爲“迭代器對象”爲“應對變化中的集合對象”提供了一種優雅的方式。迭代子(Iterator)模式又叫遊標(Cursor)模式,是對象的行爲模式。迭代子模式可以順序地訪問一個聚集中的元素而不必暴漏聚集的內部表象。


迭代子模式有兩種,白箱聚集與外稟迭代子,黑箱聚集與內稟迭代子

白箱還黑箱取決於集合本身有沒有對外提供除了迭代器以外訪問元素的接口,像Java集合裏面size,get這些方法,有的話就是白箱,如果除了迭代器接口沒有其他能夠訪問集合元素接口的就是黑箱,關於迭代子模式更多細節,可以看看這篇文章

https://www.cnblogs.com/java-my-life/archive/2012/05/22/2511506.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章