Java中迭代器所引發的思考(List中迭代器的存在一直是使我迷茫的一個點)

  正像題目所說,我剛開始學Java接觸到集合的時候,發現裏面有個迭代器,不準確的說應該是我同學告訴我裏面有個迭代器,然後說的很屌的樣子。But,說實話,我真心覺得這個迭代器的存在簡直是不可理解,比Java的泛型還扯淡,沒有絲毫存在的價值。裏面的迭代我完全可以自己寫一個簡單方法用來實習嘛,無非就遍歷一下集合給我個這麼複雜的方式幹嘛。當時因爲心思浮躁我就自欺欺人的告訴自己這屬於一種設計模式,也就自己把自己矇混過關不再考慮這個我絲毫不能理解的操作了。不過好在最近工作用的底層接口各種迭代器和C++模板搞得我想吐。也終於明白了迭代器真的是一個好的存在。


  首先咱們先不去分析Java中的迭代器的好壞,價值不大。Java集合中迭代器的存在真的是沒什麼太大的價值,不過這並不能怪Java本身,因爲就我目前的知識而言,各個語言平臺對迭代器的實現無非都是迭代一下集合中的元素,這也是我們在使用迭代器時不能明白他存在的價值的原因。那麼來舉個例子看一下,咱們來稍微感受一下迭代器的一個完美應用。
  現在呢,咱們來寫一個斐波那契的實現。具體要求是:用計算機輸出斐波那契數列的前count個元素。
  來一種最直觀的的實現:

static void printFibonacci(int count) {
        int fibonacci_n1 = 0;
        int fibonacci_n2 = 1; // 由前兩個值可以求得第三個值,所以定義兩個變量用於緩存.
        int fibonacci_n3 = 0; // 用於存儲臨時變量.
        for (int i = 0; i < count; i++) {
            System.out.println("fibonacci " + i + " :" + fibonacci_n2);
            fibonacci_n3 = fibonacci_n1 + fibonacci_n2;
            fibonacci_n1 = fibonacci_n2;
            fibonacci_n2 = fibonacci_n3;
        }
    }

  這種實現,很簡單很直接。但是這個函數除了針對本題目外其他毫無用處,因爲該函數並不能將生成的斐波那契數列提供給其他函數使用。
  而我們要給其他函數使用可以將數據存入一個List列表,並作爲返回值傳遞給外部函數使用。
  可是這樣就完美了嗎?NO!依然不完美,可以發現當count值比較小的時候是無所謂的,可是當這個值比較大的時候,List集合將會變得異常龐大,這在某些情況下是應該極力避免的情況(避免內存空間的過度浪費)。
  那麼,來一個通過迭代器進行迭代計算的實現方案:
  具體思路是寫一個迭代器,每次迭代時返回fibonacci數列中的下一個值。

public class Fibonacci {

    public static void main(String[] args) {
        // new一個迭代器對象
        FibonacciIterator iterator = new FibonacciIterator();
        int n = 30;
        // 開始迭代
        while (n-- > 0)
            System.out.println(n + " : " + iterator.next());

    }

    // 定義用於生成Fibonacci數列的迭代器
    static class FibonacciIterator implements Iterator<Integer> {

        int fibonacci_n1 = 0;
        int fibonacci_n2 = 1;
        int fibonacci_n3 = 0;

        @Override
        public boolean hasNext() {
            return true;
        }

        @Override
        public Integer next() {
            fibonacci_n3 = fibonacci_n1 + fibonacci_n2;
            fibonacci_n1 = fibonacci_n2;
            fibonacci_n2 = fibonacci_n3;
            return fibonacci_n1;
        }

        @Override
        public void remove() {

        }
    }

}

  不知道我自作多情的繼承了一個Java util包中的Iterator會不會叫大家更加迷惑,反正我是處於使它更像一個迭代器而這樣做的。雖然它本身確實就是一個迭代器,只是並沒有直接的和集合耦合在一起而已。
  可以發現在某些情況下需要一個fibonacci數列集合的時候,根本不需要耗費那麼多內存去存取,可以通過迭代器的每次迭代去進行計算,將計算的時間攤薄在每一次調用中,而且除了程序代碼以及兩三個變量以外幾乎不會再消耗任何內存空間。不知道大家有沒有感受到我當時知道這個的時候所受到的驚嚇,因爲我一直是很嫌棄迭代器的,一個沒有任何卵用的純粹增加我編碼複雜度的傢伙。沒有感受到的話看下面。
  當然圈重點,這個好玩又好用創意肯定不是我大腦靈光一閃突然想到的,因爲我一直對迭代器這個東西持有偏見,只怪自己用得少啦。。。。。然後呢,爲什麼會幡然醒悟呢!因爲這篇blog,上面的代碼也是仿照這篇blog的Java代碼的實現。

Python yield 使用淺析


清單 3. 通過 iterable 對象來迭代
for i in range(1000): pass
會導致生成一個 1000 個元素的 List,而代碼:
for i in xrange(1000): pass
則不會生成一個 1000 個元素的 List,而是在每次迭代中返回下一個數值,內存空間佔用很小。因爲 xrange 不返回 List,而是返回一個 iterable 對象。


繼續哦!!!!還有很多要說呢。
  於是這引起了我對迭代器的反思,以及在編碼過程中對迭代器的特殊需求使我對迭代器有了更加深刻的理解。下面所說的或許你會感覺雲裏霧裏,感覺自己好像是懂了可是又覺得沒有懂。不要擔心,你所需要的就是了解,知道有這個東西有這個方式就好,知道有這篇blog的存在就好。當你回頭遇到一個比較複雜的需求,當你自己都覺得自己寫的代碼有點慘不忍睹的時候,當你去思考怎麼可以把這部分代碼變得更好的時候,加入的你自己所思考的解決方案恰好似乎感覺跟迭代器有點關聯,然後你就可以再來翻翻看了,不過或許那時候你已經覺得不需要了,因爲你買了關於設計模式的書了,上面說的要比我專業很多(飄過一個大寫的尷尬)當然我寫這篇blog之前專門把迭代器模式細細的翻了一遍,可是細化這篇blog內容的時候我覺得我是不是應該再翻下創建型模式哪一張(又一個大寫的尷尬飄過)。好了,創建型下週再細細的翻,繼續我的叨逼叨。


  爲了節約大家的時間,我廢話少說,長話短說,儘量簡潔明瞭的給來家整明白嘍(叫我一個話癆少說話,真的煩)。

1. 作用

迭代器呢,咱今天不說模式只說迭代器。首先我們必須明確一個前提,迭代器出現的目的是什麼?迭代器一般會依賴於一個集合對象。一個集合對象應該提供出一種方法來讓別人訪問他的元素,而又不暴露他的內部結構。此外,針對不同的需要,還可能會以不同的方式來遍歷這個列表。但是這就存在一個問題,即使可以預見到所有的遍歷操作,你可能也不會希望列表的接口中充斥着各種不同的遍歷操作。而且有時甚至會在一個列表上同時進行多個遍歷操作,這樣要在原始集合類中緩存多少狀態位。

乾脆舉例子,什麼都沒例子來的快。
定義一個MyArrayList對象(繼承ArrayList),add進去100個元素,現在我開始提我的遍歷需求了。
  1. 我要順序遍歷。
  2. 我要逆序遍歷。
  什麼這些都好實現,直接給我在MyArrayList對象中添加next()、pre()方法就好了???
  3. 可以,那我現在要每間隔一個元素才返回一個元素。
  4. 那我現在要每間隔兩個元素才返回一個元素。
  5. 那我現在要每間隔三個元素才返回一個元素 ……
  你加吧,看你能在MyArrayList中給我寫多少方法,行就算你不嫌麻煩覺得依然可以維護。那我現在又有一個需求。
  6. 我要順序遍歷這個列表的同時還能逆序遍歷等,同一時間我要使用三種、四種……遍歷方案。
  這個時候你怎麼實現,在MyArrayList中爲每一種遍歷方式定義一個狀態值嗎?累不累,真的不好管理這樣。。。。。
  同時,迭代器也可以很方便的實現兩個集合類的內容比較。

  那麼迭代器的作用簡而言之就是:幫助你將對列表的訪問和遍歷從列表對象中分離出來。將分離出來的方法還要狀態值放入一個類中,這個類就是迭代器。

2. 迭代器的構成

  其實這個或許應該放在前面說。迭代器的構成呢,很簡單,前面也有提到,迭代器往往依附於一個數據集合,就是用來操作一個聚合對象數據內容的。
  迭代器呢首先肯定存在一個獲取當前元素的操作,迭代器中最重要的一個是,迭代器的遍歷算法,這也是迭代器的心臟,Java中List派生類中對Iterator的遍歷算法實現都是逐個遍歷,也正是因爲這樣,使愚鈍的我陷入了局限性思維,以爲迭代器就僅僅是用來進行逐個遍歷的,沒有拓展到當出現其他遍歷算法時使用迭代器的便捷性,更沒意識到迭代器的真正效用是在我們程序員手裏的時候,而不是在util包中的時候。心臟的作用就是泵血,有了算法這個心臟,迭代器還必須有一管子的數據集合作爲血液。
  這就清晰明瞭了,迭代器由兩部分組成:數據集合和迭代算法。

3. 迭代器的實現(感覺還是跑回設計模式了)

  基於以上迭代器結構的認識,我相信迭代器的實現對大家來說就很簡單了。無非就是將集合數據給到迭代器,然後實現遍歷算法即可。一個迭代器就這樣實現了,可是爲了代碼的複用,以及代碼結構的統一(這種統一方便我們對Java多態的使用。不知道這句話是不是很有問題,大家理解就好),在Java中我們往往不直接定義一個迭代器對象。而是定義一個迭代器接口,整個項目通過實現統一的迭代器接口來實現一個迭代器類。這樣我們就可以很方便的實現兩個集合數據的重合度比較了。至於怎麼方便搭建可以自行看一下AbstractList的equals(Object o) 函數(我將其中的代碼稍微簡化了下)

public boolean equals(List list) {
        ListIterator<Object > e1 = listIterator();
        ListIterator<Object > e2 = list.listIterator();
        while (e1.hasNext() && e2.hasNext()) {
            Object  o1 = e1.next();
            Object o2 = e2.next();
            if (!(o1==null ? o2==null : o1.equals(o2)))
                return false;
        }
        return !(e1.hasNext() || e2.hasNext());
    }

  下面咱們根據迭代器結構進行分開討論怎麼構建一個迭代器(要實現餘以及接口,或者繼承自某個基類這個就不說了,對於繼承大家用的說不定比我溜)。迭代器的結構中有兩個東西,一個數據集合,一個迭代算法。
  首先從數據集合開始
  Iterator因爲要對數據集合進行操作,所以Iterator中必須存在這個集合的引用(不是副本哦,副本的話數據變化你還得自己同步數據)。那要拿到這個集合的引用就有兩種方式:
  1. 最直接的方式就是通過構造函數傳入一個集合對象。嫌麻煩的話可以看第二種方式。
  2. 像Java中的集合類的實現那樣,將Iterator定義到集合類內部,作爲一個內部類實現,這樣將自動拿到集合類對象的引用。
  再就是迭代算法了
  實現不同的迭代算法其實就是支持不同的操作,不同的對列表各個元素的操作。此時呢,也是有兩種方式可供選擇。
  1. 最直接的方式就是每一種迭代算法定義一個迭代器,可是這種方式在某些情況下就會很雞肋,比如有兩個集合類,這兩個集合類的數據結構並不相同,可是又都需要一個相同迭代算法的迭代器實現,那樣豈不是要爲每一個類寫一個相應的迭代器實現,這是很煩的。看第二種方法。
  2. 傳方法,給迭代器傳過去一個方法,該方法用於迭代算法的實現,可是Java中暫時並不支持直接傳函數過去,只能通過定義一個通用接口對象傳遞給Iterator進行調用(比如說自己定義一個Iteratable接口,裏面有一個Iterator_algorithm方法,然後我只需要在Iterator中的響應方法中調用Iterator_algorithm方法即可)。這種方法雖然看起來蠻不錯,可是要設計的好也不容易,需要根據項目的具體需求去設計改進。設計Iteratable接口的準則應該是,在Iteratable中不進行變量的定義,不進行復雜參數的傳遞,總之一句話Iteratable接口的實現儘量只用於迭代算法的計算邏輯實現,不要與數據集合產生任何耦合
。如果逼不得已必須會產生耦合那就需要根據具體項目進行具體分析採用什麼方式了。
  爲什麼我會很欣賞第二種方式,因爲它足夠簡單,通過第二種方式,你不需要寫迭代的具體方式,而只需要交給上層使用該迭代器的人員對迭代算法進行實現。極大地避免了因方法實現而產生的摩擦。當然你或許會說通過方式一也一樣可以交給外層人員,yes,可是這個時候外層人員新建了一個Iterator,那麼就不能通過很方便的內部類來實現Iterator了,而是要在創建Iterator對象的時候將List集合對象作爲參數傳入。當然外部人員還可以採用將新實現的Iterator類的class對象傳遞給集合對象,然後在集合對象內部通過反射進行實現,這樣代碼看起來依然像Java的結構一樣,可是你不嫌麻煩的話怎麼來都行。
  各個語言平臺雖然有很大差異,但是都不會妨礙你的任何一個需求的實現,只是在有些語言平臺上你的需求實現過程會繞很多彎彎,在有些平臺則會很順利很簡單。同樣的在同一個語言平臺同一個需求也往往會有很多種不同的解決方案,這些方案或許相比於其他平臺都很複雜,又或者有些簡單有些複雜,具體怎麼實現不能一概而論,需要根據項目實況進行分析,選擇最通用、最便捷好用的方式進行就行。

  原諒我後面沒有貼代碼進行更好的講解,因爲你真的會自己明白的。還有學習任何東西都不要慌,時間線拉的要長一點,慌是慌不來的。切莫急功急利,主要提醒我自己。

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