1. 程式人生 > 實用技巧 >【設計模式】【行為型】【迭代器模式】Iterator Design Pattern

【設計模式】【行為型】【迭代器模式】Iterator Design Pattern

迭代器模式(Iterator Design Pattern)

用來遍歷集合物件。這裡說的“集合物件”也可以叫“容器”“聚合物件”,實際上就是包含一組物件的物件,比如陣列、連結串列、樹、圖、跳錶。迭代器模式將集合物件的遍歷操作從集合類中拆分出來,放到迭代器類中,讓兩者的職責更加單一

迭代器模式的原理和實現

// 介面定義方式一
public interface Iterator<E> {
  boolean hasNext();
  void next();
  E currentItem();
}

// 介面定義方式二
public interface Iterator<E> {
  boolean hasNext();
  E next();
}

public interface List { Iterator iterator(); //...省略其他介面函式...}

迭代器模式的優勢

  • for 迴圈
  • foreach 迴圈,foreach 迴圈只是一個語法糖而已,底層是基於迭代器來實現的。
  • iterator 迭代器
  • 對於類似陣列和連結串列這樣的資料結構,遍歷方式比較簡單,直接使用 for 迴圈來遍歷就足夠了。但是,對於複雜的資料結構(比如樹、圖)來說,有各種複雜的遍歷方式。比如,樹有前中後序、按層遍歷,圖有深度優先、廣度優先遍歷等等。如果由客戶端程式碼來實現這些遍歷演算法,勢必增加開發成本,而且容易寫錯。如果將這部分遍歷的邏輯寫到容器類中,也會導致容器類程式碼的複雜性。
  • 我們可以將遍歷操作拆分到迭代器類中。比如,針對圖的遍歷,我們就可以定義 DFSIterator、BFSIterator 兩個迭代器類,讓它們分別來實現深度優先遍歷和廣度優先遍歷。
  • 將遊標指向的當前位置等資訊,儲存在迭代器類中,每個迭代器獨享遊標資訊。這樣,我們就可以建立多個不同的迭代器,同時對同一個容器進行遍歷而互不影響。
  • 容器和迭代器都提供了抽象的介面,方便我們在開發的時候,基於介面而非具體的實現程式設計。當需要切換新的遍歷演算法的時候,比如,從前往後遍歷連結串列切換成從後往前遍歷連結串列,客戶端程式碼只需要將迭代器類從 LinkedIterator 切換為 ReversedLinkedIterator 即可,其他程式碼都不需要修改。除此之外,新增新的遍歷演算法,我們只需要擴充套件新的迭代器類,也更符合開閉原則。

在遍歷的同時增刪集合元素會發生什麼?

  • 在通過迭代器來遍歷集合元素的同時,增加或者刪除集合中的元素,有可能會導致某個元素被重複遍歷或遍歷不到。

如何實現一個支援“快照”功能的迭代器模式?

  • 所謂“快照”,指我們為容器建立迭代器的時候,相當於給容器拍了一張快照(Snapshot)。之後即便我們增刪容器中的元素,快照中的元素並不會做相應的改動。而迭代器遍歷的物件是快照而非容器,這樣就避免了在使用迭代器遍歷的過程中,增刪容器中的元素,導致的不可預期的結果或者報錯。
  • 解決方案一:每當建立迭代器的時候,都拷貝一份容器中的元素到快照中,後續的遍歷操作都基於這個迭代器自己持有的快照來進行。
  • 解決方案二:我們可以在容器中,為每個元素儲存兩個時間戳,一個是新增時間戳 addTimestamp,一個是刪除時間戳 delTimestamp。當元素被加入到集合中的時候,我們將 addTimestamp 設定為當前時間,將 delTimestamp 設定成最大長整型值(Long.MAX_VALUE)。當元素被刪除時,我們將 delTimestamp 更新為當前時間,表示已經被刪除。
    • 注意,這裡只是標記刪除,而非真正將它從容器中刪除。同時,每個迭代器也儲存一個迭代器建立時間戳 snapshotTimestamp,也就是迭代器對應的快照的建立時間戳。當使用迭代器來遍歷容器的時候,只有滿足 addTimestamp<snapshotTimestamp<delTimestamp 的元素,才是屬於這個迭代器的快照。
    • 如果元素的 addTimestamp>snapshotTimestamp,說明元素在建立了迭代器之後才加入的,不屬於這個迭代器的快照;如果元素的 delTimestamp<snapshotTimestamp,說明元素在建立迭代器之前就被刪除掉了,也不屬於這個迭代器的快照。
    • 上面的解決方案相當於解決了一個問題,又引入了另外一個問題。ArrayList 底層依賴陣列這種資料結構,原本可以支援快速的隨機訪問,在 O(1) 時間複雜度內獲取下標為 i 的元素,但現在,刪除資料並非真正的刪除,只是通過時間戳來標記刪除,這就導致無法支援按照下標快速隨機訪問了。
    • 我們可以在 ArrayList 中儲存兩個陣列。一個支援標記刪除的,用來實現快照遍歷功能;一個不支援標記刪除的(也就是將要刪除的資料直接從陣列中移除),用來支援隨機訪問。對應的程式碼我這裡就不給出了,感興趣的話你可以自己實現一下。