(十七)迭代器模式詳解(foreach的精髓)


                  作者:zuoxiaolong8810(左瀟龍),轉載請注明出處,特別說明:本博文來自博主原博客,為保證新博客中博文的完整性,特復制到此留存,如需轉載請注明新博客地址即可。

                  各位好,很久沒以LZ的身份和各位對話了,前段時間為了更加逼真的解釋設計模式,LZ費盡心思給設計模式加入了故事情節,本意是為了讓各位在看小說的過程中就可以接觸到設計模式,不過寫到現在,LZ最深的感觸就是,構思故事的時間遠遠超過了LZ對設計模式本身的研究。

                  本章介紹迭代器模式,不再采用故事嵌入的講解方式,主要原因是因為迭代器模式本身有更多需要介紹的東西,如果嵌入到小說當中,會不太方便去闡述這些內容。

                  另外,由於LZ的大部分設計模式文章主要針對的人群是對設計模式已經有一定了解,希望更加深入理解的程序猿們。所以LZ希望各位在看本篇文章時,可以打開一個初步介紹迭代器模式的文章,對比觀看。下面進入正題,我們先來看看百度百科或者說GOF對迭代器模式的定義。

                  定義:提供一種方法順序訪問一個聚合對象中各個元素,而又不需暴露該對象的內部表示。

                  從定義中可以看出,迭代器模式是為了在不暴露該對象內部表示的情況下,提供一種順序訪問聚合對象中元素的方法。這種思想在JAVA集合框架中已經體現的淋漓盡致,而且LZ相信每一個接觸JAVA的同學都難免要去觸碰。

                  所以LZ這次先不給出迭代器的類圖與標准實現,我們先來看看迭代器模式解決了JAVA集合框架中的哪些問題。

                  為了更加清晰,LZ斗膽寫了幾個簡單的集合類(向JDK類庫的締造者致敬),我們從這幾個簡單的集合類出發,去仔細體會下定義的意思,下面是LZ分別寫的縮小版的ArrayList、LinkedList和HashSet。

package com.iterator;

public class ArrayList<E> {

    private static final int INCREMENT = 10;
    
    private E[] array = (E[]) new Object[10];
    
    private int size;
    
    public void add(E e){
        if (size < array.length) {
            array[size++] = e;
        }else {
            E[] copy = (E[]) new Object[array.length + INCREMENT];
            System.arraycopy(array, 0, copy, 0, size);
            copy[size++] = e;
            array = copy;
        }
    }
    
    public Object[] toArray(){
        Object[] copy = new Object[size];
        System.arraycopy(array, 0, copy, 0, size);
        return copy;
    }
    
    public int size(){
        return size;
    }
}
package com.iterator;

public class LinkedList<E> {

    private Entry<E> header = new Entry<E>(null, null, null);
    private int size;
    
    public LinkedList() {
        header.next = header.previous = header;
    }
    
    public void add(E e){
        Entry<E> newEntry = new Entry<E>(e, header, header.next);
        newEntry.previous.next = newEntry;
        newEntry.next.previous = newEntry;
        size++;
    }
    
    public int size(){
        return size;
    }
    
    public Object[] toArray(){
        Object[] result = new Object[size];
        int i = size - 1;
        for (Entry<E> e = header.next; e != header; e = e.next)
            result[i--] = e.value;
        return result;
    }
    
    private static class Entry<E>{
        E value;
        Entry<E> previous;
        Entry<E> next;
        public Entry(E value, Entry<E> previous, Entry<E> next) {
            super();
            this.value = value;
            this.previous = previous;
            this.next = next;
        }
    }
    
}
package com.iterator;

import java.util.HashMap;
import java.util.Map;

public class HashSet<E> {

    private static final Object NULL = new Object();
    
    private Map<E, Object> map = new HashMap<E, Object>();
    
    public void add(E e){
        map.put(e, NULL);
    }
    
    public int size(){
        return map.size();
    }
    
    public Object[] toArray(){
        return map.keySet().toArray();
    }
}

                下面我們看看三個類的遍歷方式。

package com.iterator;


public class Main {

    public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<Integer>();
        for (int i = 1; i <= 11; i++) {
            arrayList.add(i);
        }
        System.out.println("arrayList size:" + arrayList.size());
        Object[] arrayListArray = arrayList.toArray();
        for (int i = 0; i < arrayListArray.length; i++) {
            System.out.println(arrayListArray[i]);
        }
        
        System.out.println("----------------------------------------------");
        
        HashSet<Integer> hashSet = new HashSet<Integer>();
        for (int i = 1; i <= 11; i++) {
            hashSet.add(i);
        }
        System.out.println("hashSet size:" + hashSet.size());
        Object[] setArray = hashSet.toArray();
        for (int i = 0; i < setArray.length; i++) {
            System.out.println(setArray[i]);
        }
        
        System.out.println("----------------------------------------------");
        
        LinkedList<Integer> linkedList = new LinkedList<Integer>();
        for (int i = 1; i <= 11; i++) {
            linkedList.add(i);
        }
        System.out.println("linkedList size:" + linkedList.size());
        Object[] linkedListArray = linkedList.toArray();
        for (int i = 0; i < linkedListArray.length; i++) {
            System.out.println(linkedListArray[i]);
        }
    }
}

                結果在這里LZ就不貼了,只是簡單的輸出三次1到11。

                各位思考一下,我們這里的遍歷是如何做到的。很明顯,我們是通過一個通用的方法,toArray做到的。當然,為了迎合面向接口的思想,你可以添加一個接口規定toArray的行為,讓三個類去實現它。

                但是在這里有一個很大的弊端,不知道各位注意到沒有,那就是不論我們的集合類是如何實現的(比如鏈表,數組,散列),在使用數組遍歷集合類的時候,我們其實遍歷了兩次。

                在這三個類中,由於System的arraycopy和set的toArray方法是黑箱子,所以最明顯的便是LinkedList的實現,它是先遍歷了一遍鏈表,做出來一個數組,然后當客戶端獲得到這個數組的時候,則需要再來一次循環,去遍歷每一個元素。

                為何會是這種情況呢?

                很簡單,因為我們的集合類本身就不是一個數組,所以自然要多一步從集合類到數組的過渡。哪怕是本身由數組實現的ArrayList,也避免不了多這一步,各位可以試一下在ArrayList中直接返回array屬性,結果中會出現一堆null值,而且這樣做的話,對於array的改變會直接影響到ArrayList本身,這並不是我們所希望看到的,所以我們返回的只是一個拷貝。

                當然,為了解決這個問題,我們並不是沒有辦法,比如給LinkedList和ArrayList加入get方法,而這個方法有一個參數index,這是我們常用的遍歷方法。如此一來,便解決了二次遍歷的問題。

                但是問題又來了,那就是我們無法給HashSet提供一個根據索引獲取元素的方法,由於散列特性的緣故,set中的元素是無序的,或者說順序是不被保證的。那么這個get方法,在HashSet中便無法提供,因為這里沒有我們通俗意義上的索引的概念。

                可以看到,上面LZ粗淺的分析,得出一個結論。三個集合類,如果統一提供數組給客戶端遍歷,那么在遍歷過程中會出現重復遍歷的現象。而如果消除這種重復遍歷,則由於內部數據結構的不同,三個集合類無法做到像提供數組一樣,給客戶端提供統一的遍歷方式。

                為了解決上面的問題,迭代器模式就隨之出現了。我們先來看看迭代器模式在百度百科中的類圖,稍后各位可以自己體會下,迭代器模式是否解決了上面的問題,以及是否提供了額外的一些好處。

                   看着上面的類圖,我們可以分析出來,上面我們所寫的ArrayList等三個類都屬於ConcreteAggregate的位置。如果我們剛才設計一個數組接口讓三個類去實現的話,其實已經和迭代器模式十分相似了。他們的類圖會是下面這樣的。


                      在上述類圖中,我們從面向對象的角度思考,將Object[]當做一個對象對待,我們對比下兩個類圖,他們其實是非常相似的,其中最大的區別在於,第二個類圖當中,沒有抽象數組接口這個概念,而在迭代器模式的類圖中,是有迭代器接口這個概念的。

                      上述區別最終所造成的結果就是,由於數組是以固定的排列方式存在的,即數組必須是一組連續的內存區域(邏輯上連續),故而以數組為基礎的遍歷方式只能是按照索引遍歷。而迭代器則不限制,我們注意到,在迭代器模式的類圖中,具體的迭代器是有一條到具體聚合對象的關聯線的,這就意味着迭代器的實現是與具體的聚合對象息息相關的,也就是說迭代器滿足了多種迭代方式。

                      好了,截止到目前,我們前面所討論的都是為何要使用迭代器模式,或者說迭代器模式解決了哪些問題。我們來稍微總結一下。

                      1、迭代器模式可以提供統一的迭代方式,這個要歸功於Iterator接口。

                      2、迭代器模式可以在對客戶透明的前提下,做出各種不同的迭代方式。

                      3、在迭代的時候不需要暴露聚合對象的內部表示,我們只需要認識Iterator即可。

                      4、在第1條的前提下,解決了基於數組的迭代方式中重復遍歷的問題。

                      這里LZ就不再給出迭代器模式的標准代碼實現了,如果各位看過LZ的前十幾篇設計模式,會發現,LZ其實很多時候是不寫標准實現的,一個是因為網上的這種資料很多,很容易找到,LZ不想重復造輪子。還有一個重要的原因是,標准實現總難免給人死板硬套的感覺,很難讓人理解,至少LZ個人當時是這種感覺。

                      這里LZ直接使用迭代器模式,將我們上面的三個集合類稍微優化一下,首先我們應該寫一個迭代器接口,它大概會有類圖中的那幾個方法。為了簡單起見,我們直接利用JDK提供的Iterator接口,源碼如下。

public interface Iterator<E> {

    boolean hasNext();

    E next();

    void remove();
}

                    這里迭代器接口已經有了,我們還需要一個可迭代的類接口,在JDK中相當於Iterable接口,它規定了返回一個迭代器的行為,與我們的類圖中Array接口類似,只不過那里是toArray方法。下面我們引用JDK中的Iterable接口,非常簡單,源碼如下。

public interface Iterable<T> {

    Iterator<T> iterator();
}

                    下面我們就讓三個集合類全部提供一個方法,可以返回一個Iterator實例,並且實現Iterable接口。

package com.iterator;

import java.util.Iterator;

public class ArrayList<E> implements Iterable<E>{

    private static final int INCREMENT = 10;

    private E[] array = (E[]) new Object[10];

    private int size;

    public void add(E e) {
        if (size < array.length) {
            array[size++] = e;
        } else {
            E[] copy = (E[]) new Object[array.length + INCREMENT];
            System.arraycopy(array, 0, copy, 0, size);
            copy[size++] = e;
            array = copy;
        }
    }

    public Object[] toArray() {
        Object[] copy = new Object[size];
        System.arraycopy(array, 0, copy, 0, size);
        return copy;
    }

    public int size() {
        return size;
    }

    public Iterator<E> iterator() {
        return new Itr();
    }

    private class Itr implements Iterator<E> {

        int cursor = 0;

        public boolean hasNext() {
            return cursor != size();
        }

        public E next() {
            return array[cursor++];
        }

        public void remove() {
            
        }

    }
}
package com.iterator;

import java.util.Iterator;

public class LinkedList<E> implements Iterable<E>{

    private Entry<E> header = new Entry<E>(null, null, null);
    private int size;
    
    public LinkedList() {
        header.next = header.previous = header;
    }
    
    public void add(E e){
        Entry<E> newEntry = new Entry<E>(e, header, header.next);
        newEntry.previous.next = newEntry;
        newEntry.next.previous = newEntry;
        size++;
    }
    
    public int size(){
        return size;
    }
    
    public Object[] toArray(){
        Object[] result = new Object[size];
        int i = size - 1;
        for (Entry<E> e = header.next; e != header; e = e.next)
            result[i--] = e.value;
        return result;
    }
    
    private static class Entry<E>{
        E value;
        Entry<E> previous;
        Entry<E> next;
        public Entry(E value, Entry<E> previous, Entry<E> next) {
            super();
            this.value = value;
            this.previous = previous;
            this.next = next;
        }
    }
    
    public Iterator<E> iterator() {
        return new Itr();
    }

    private class Itr implements Iterator<E> {

        Entry<E> current = header;

        public boolean hasNext() {
            return current.previous != header;
        }

        public E next() {
            E e = current.previous.value;
            current = current.previous;
            return e;
        }

        public void remove() {
            
        }

    }
    
}
package com.iterator;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class HashSet<E> implements Iterable<E>{

    private static final Object NULL = new Object();
    
    private Map<E, Object> map = new HashMap<E, Object>(7,1);
    
    public void add(E e){
        map.put(e, NULL);
    }
    
    public int size(){
        return map.size();
    }
    
    public Object[] toArray(){
        return map.keySet().toArray();
    }
    
    public Iterator<E> iterator() {
        return map.keySet().iterator();
    }

}

                這下我們已經將迭代器模式應用到了我們上面的例子當中,至於remove方法,我們為了簡短清晰,就不做實現了,而且它並不影響我們理解迭代器模式。現在我們客戶端的迭代方式就可以改變一下了。可以像下面這樣迭代。

package com.iterator;

import java.util.Iterator;


public class Main {

    public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<Integer>();
        for (int i = 1; i <= 11; i++) {
            arrayList.add(i);
        }
        System.out.println("arrayList size:" + arrayList.size());
        Iterator<Integer> arrayListIterator = arrayList.iterator();
        while(arrayListIterator.hasNext()) {
            System.out.println(arrayListIterator.next());
        }
        
        System.out.println("----------------------------------------------");
        
        HashSet<Integer> hashSet = new HashSet<Integer>();
        for (int i = 1; i <= 11; i++) {
            hashSet.add(i);
        }
        System.out.println("hashSet size:" + hashSet.size());
        Iterator<Integer> hashSetIterator = hashSet.iterator();
        while(hashSetIterator.hasNext()) {
            System.out.println(hashSetIterator.next());
        }
        
        System.out.println("----------------------------------------------");
        
        LinkedList<Integer> linkedList = new LinkedList<Integer>();
        for (int i = 1; i <= 11; i++) {
            linkedList.add(i);
        }
        System.out.println("linkedList size:" + linkedList.size());
        Iterator<Integer> LinkedListIterator = linkedList.iterator();
        while(LinkedListIterator.hasNext()) {
            System.out.println(LinkedListIterator.next());
        }
    }
}

                其中輸出的結果與第一例是一樣的,都是將1到11輸出三遍,可以明顯的看出,我們剛才的重復遍歷問題不見了,而且三個集合類的迭代方法是一樣的,而有了這個特點,JAVA在此基礎上,給我們提供了foreach語法,所以我們可以寫成下面這樣。

package com.iterator;

public class Main {

    public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<Integer>();
        for (int i = 1; i <= 11; i++) {
            arrayList.add(i);
        }
        System.out.println("arrayList size:" + arrayList.size());
        for (Integer i : arrayList) {
            System.out.println(i);
        }
        
        System.out.println("----------------------------------------------");
        
        HashSet<Integer> hashSet = new HashSet<Integer>();
        for (int i = 1; i <= 11; i++) {
            hashSet.add(i);
        }
        System.out.println("hashSet size:" + hashSet.size());
        for (Integer i : hashSet) {
            System.out.println(i);
        }
        
        System.out.println("----------------------------------------------");
        
        LinkedList<Integer> linkedList = new LinkedList<Integer>();
        for (int i = 1; i <= 11; i++) {
            linkedList.add(i);
        }
        System.out.println("linkedList size:" + linkedList.size());
        for (Integer i : linkedList) {
            System.out.println(i);
        }
    }
}

                這樣一來,我們不管你是基於何種數據結構提供的集合類,我們只管foreach遍歷,迭代器模式對JAVA集合框架做出的貢獻不可謂不大。下面LZ帶各位看看現在的類圖是如何。

 


                從類圖中可以清楚的看出,與迭代器模式的類圖是一模一樣的,當然,客戶端與Iterable的依賴關系有待商議,之前我們已經提到過,JAVA集合框架的工廠方法模式是非透明的處理方式,所以我們很多時候不會使用Iterable,不過這並不影響我們對迭代器模式的理解。

                然而迭代器模式所帶來的好處已經不言而喻,上面分析的過程中已經提到過,LZ這里不再贅述。

                值得注意的是,LZ全部采用的內部類作為各個集合類迭代器的實現,這在LZ之前的文章中已經提到過,當時的解釋是說內部類是為了完全杜絕客戶端對迭代器實現類的依賴,而進行到現在,我們可以更深一步討論。

                這里我們的理解是,內部類在這里目的是為了隱藏實現細節,並且如此一來,迭代器的實現類可以自由的使用集合類的各個屬性,而不需要集合類提供自己屬性訪問的接口以及建立二者的關聯關系,這種感覺十分像C/C++中的友元類。

                不過缺點也接踵而至,由於具體的集合類與具體的迭代器是綁定的關系,所以這種實現方式在復用的過程中會有很大的限制甚至是不能復用,這個缺點對於C/C++中的友元類來說,是不存在的。


                到此,迭代器模式的介紹就基本結束了,希望各位可以從LZ的分析當中得到一點益處,LZ便十分知足了。下面是對於foreach語法的深入討論,新手或者已經了解的同學們可以忽略,當然,如果新手們有耐心,也可以繼續看下去。

                JAVA的foreach語法中,冒號后面的變量可以是兩種,一個是數組,一個便是實現了Iterable接口的任何類,數組的迭代方式本次不在討論之列,我們主要關注實現了Iterable接口的類,是如何使用foreach遍歷的。

                我們使用JDK中自帶的一個工具javap,去獲得剛才Main類的class文件描述,去看看遍歷的過程。我們在命令行模式下,進入到Main類class文件的目錄中,然后執行下面的命令,為了更加清晰,我們只保留ArrayList的代碼片段,將其余兩部分刪掉或者注釋掉。


                執行這個命令,我們可以看到在main函數中有關foreach的JVM指令,我們來看下JVM是怎么執行foreach的,生成的有關內容如下。


                   各位的行數不一定會和LZ一樣,不過總能找到和LZ一樣的JVM指令片段,下面LZ直接按照上面的行號去解釋每一行都做了什么事情。

                   56:執行arrayList的iterator方法。

                   59:將返回值從值棧賦給第四個引用變量,這個是在解釋執行過程中自動加入的臨時變量。

                   60:跳到80行。

                   80:將第四個引用變量壓入值棧,這個引用變量是個迭代器,也就是iterator方法的返回值。

                   81:執行接口方法hasNext。

                   86:當hasNext方法返回的值不等於0時,也就是不為false時,跳到63行。

                   63:將第四個引用變量壓入值棧。

                   64:執行接口方法next。

                   69:檢查強轉,不成功將ClassCastException。

                   72:將next返回的值強轉后賦給第三個引用變量,在代碼里相當於i。

                   73:獲取out輸出流靜態屬性,壓入值棧。

                   76:將i壓入值棧。

                   77:執行println方法,打印i。

                   80:同上面的80行。


                   到這里,可以看到,JVM對於foreach的執行過程,其實相當於下面的代碼。

        //其中iterator這個變量就是第四個引用變量,i是第三個。
        for (Iterator iterator = arrayList.iterator(); iterator.hasNext();) {
            Integer i = (Integer) iterator.next();
            System.out.println(i);
        }

                   這么做的前提自然是建立在iterable接口和iterator接口的基礎之上,由於這兩者規定了集合類的行為,所以JAVA才得以利用這一特性,提供給開發者foreach的語法。

 

                   對於foreach的語法就分析到這里了,最后我們再對迭代器模式進行一下總結。

                   迭代器模式是為了在隱藏聚合對象的內部表示的前提下,提供一種遍歷聚合對象元素的方法。這在之前的例子當中已經有體現,我們給三個集合類提供了統一的遍歷方式,消除了重復遍歷,最終還使用內部類將細節包裝在集合類內部隱藏起來,使得外部無法訪問集合類的任何一個屬性。

                   這當中還使用到了工廠方法模式,這個在工廠方法模式一章中,LZ已經提到過,對於迭代器的產生,是工廠方法模式處理的。兩個設計模式互相結合,讓JAVA的集合框架更加優美、健壯。

                   LZ如此詮釋迭代器模式,希望對大家有幫助,好了,咱們下期再見吧。





 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM