迭代器模式(Iterator)
走遍天下,世界那么大,我想去看看
在計算機中,Iterator意為迭代器,迭代有重復的含義,在程序中,更有“遍歷”的含義
如果給定一個數組,我們可以通過for循環來遍歷這個數組,這種遍歷就叫做迭代
對於數組這種數據結構,我們稱為是可迭代的
所以
迭代器就是可以用來對於一個數據集合進行遍歷的對象
意圖
提供一種方法,順序訪問一個聚合對象中的各個元素,而又不需要暴露該對象的內部表示。
別名:游標 Cursor
集合與遍歷
由一個或多個確定的元素所構成的整體叫做集合。
多個對象聚集在一起形成的總體稱之為聚集aggregate。
集合和聚集有相似的含義。
容器是指盛物品的器具,Java的Collection框架,就是設計用來保存對象的容器
容器可以認為是集合、聚集的具體體現形式,三個元素在一起叫做集合(聚集),怎么在一起?數組,列表?這具體的體現形式就是容器
容器必須提供內部對象的訪問方式,如果不能獲取對象,容器也失去了存在的意義,一個只能進不能出的儲蓄罐你要它何用?
因為容器的存在就是為了更方便的使用、管理對象。
而且通常需要容器提供對於內部所有元素的遍歷方法。
然而容器其內部有不同的擺放形式,可順序,可無序堆集
簡言之,就是不同類型的容器必然有不同的內部數據結構
那么,一種解決辦法就是不同的容器各自提供自己的遍歷方法。
這樣的話,對於使用容器管理對象的客戶端程序來說:
如果迭代的邏輯,也就是業務邏輯沒有變化
當需要更換為另外的集合時,就需要同時更換這個迭代方法
考慮這樣一個場景
有一個方法,方法的參數類型為 Collection
他的迭代邏輯,也就是業務邏輯為遍歷所有元素,讀取每個元素的信息,並且進行打印...
如果不同的容器有不同的遍歷方法,也就是一種實現類有一種不同的遍歷方法
一旦更換實現類,那么就需要同步更換掉這個迭代方法,否則方法將無法通過編譯
有一個方法,方法的參數類型為 Collection
他的迭代邏輯,也就是業務邏輯為遍歷所有元素,讀取每個元素的信息,並且進行打印...
如果不同的容器有不同的遍歷方法,也就是一種實現類有一種不同的遍歷方法
一旦更換實現類,那么就需要同步更換掉這個迭代方法,否則方法將無法通過編譯
如果集合的實現不變,需要改變業務邏輯,也就是迭代的邏輯
那么就需要修改容器類的迭代方法,也就是修改原來的遍歷方法
還是上面的場景
有一個方法,方法的參數類型為 Collection
他的迭代邏輯,也就是業務邏輯為遍歷所有元素,讀取每個元素的信息,並且進行打印...
現在他的實現類無需變化
但是業務邏輯需要變動,比如希望從后往前的方式進行遍歷,而不再是從前往后
就需要修改原來的方法或者重新寫一個方法
出現上述問題的根本原因就在於元素的迭代邏輯與容器本身耦合在一起
當迭代邏輯或者集合實現發生變更時,需要進行修改,不符合開閉原則
容器自身不僅僅需要存儲管理對象,還要負責對象的遍歷訪問,不符合單一職責原則
存儲管理對象是容器的核心職責,雖然經常需要提供遍歷方法,但是他並不是核心職責
但是為了提供遍歷元素的方法,可能不得不在容器類內提供各種全局變量,比如保存當前的位置等,這無疑也會導致容器聚集類設計的復雜度
結構
抽象迭代器角色Iterator
定義遍歷元素所需要的接口
具體的迭代器ConcreteIterator
實現了Iterator接口,並且跟蹤當前位置
抽象集合容器角色Aggregate
定義創建相應迭代器的接口(方法)
就是一個容器類,並且定義了一個返回迭代器的方法
具體的容器角色ConcreteAggregate
Aggregate的子類,並且實現了創建Iterator對象的接口,也就是返回一個ConcreteIterator實例
客戶端角色Client
持有容器對象以及迭代器對象的引用,調用迭代對象的迭代方法遍歷元素
迭代器模式中,通過一個外部的迭代器來對容器集合對象進行遍歷。
迭代器定義了遍歷訪問元素的協議方式。
容器集合對象提供創建迭代器的方法。
示例代碼
Aggregate角色
提供了iterator()獲取Iterator
package iterator; public abstract class Aggregate { abstract Iterator iterator(); abstract Object get(int index); abstract int size(); }
ConcreateAggregate角色
內部使用一個Object數組,數組直接通過構造方法傳遞進去(只是為了演示學習模式,不要糾結這算不上一個容器)
提供了大小的獲取方法以及獲取指定下標元素的方法
尤其是實現了iterator()方法,創建一個ConcreteIterator實例,將當前ConcreteAggregate作為參數傳遞給他的構造方法
package iterator; public class ConcreateAggregate extends Aggregate { private Object[] objects; ConcreateAggregate(Object[] objects) { this.objects = objects; } @Override Iterator iterator() { return new ConcreateIterator(this); } @Override Object get(int index) { return objects[index]; } @Override int size() { return objects.length; } }
迭代器接口
一個是否還有元素的方法,一個獲取下一個元素的方法
package iterator; public interface Iterator { boolean hasNext(); Object next(); }
具體的迭代器
內部維護了數據的大小和當前位置
如果下標未到最后,那么就是還有元素
next()方法用於獲取當前元素,獲取后當前位置往后移動一下
package iterator; public class ConcreateIterator implements Iterator { private Aggregate aggregate; private int index = 0; private int size = 0; ConcreateIterator(Aggregate aggregate) { this.aggregate = aggregate; size = aggregate.size(); } @Override public boolean hasNext() { return index < size ? true : false; } @Override public Object next() { Object value = aggregate.get(index); index++; return value; } }
測試類
package iterator; public class Client { public static void main(String[] args) { Object[] objects = {"1", 2, 3, 4, 5}; Aggregate aggregate = new ConcreateAggregate(objects); Iterator iterator = aggregate.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } } }
示例代碼中ConcreateAggregate本身提供了獲取指定下標元素的方法,可以直接調用獲取元素
借助於Iterator,將迭代邏輯從Aggregate中剝離出來,獨立封裝實現
在客戶端與容器之間,增加了一層Iterator,實現了客戶端程序與容器的解耦
說白了,增加了Iterator,相當於通過Iterator封裝了真實容器對象的獲取元素的方法
不直接調用方法,經過Iterator轉換一層
而且仔細品味下,這有點“適配”的韻味,適配的目標就是統一的元素訪問協議,通過Iterator約定
而被適配的角色,則是真實容器對象元素的操作方法
總之“間接”“委托”“代理”的感覺,對吧,好處自己品味
外部迭代與內部迭代
在上面的示例程序中,通過引入Iterator,實現了迭代邏輯的封裝抽象
但是容器聚集對象本身有獲取元素的方法,所以客戶端仍舊可以自行遍歷
Iterator也只不過是容器聚集對象的一個客戶而已
這種迭代器也叫做外部迭代器
對於外部迭代器有一個問題,對於不同的ConcreteAggregate,可能都需要一個不同的ConcreteIterator
也就是很可能會不得不創建了一個與Aggregate等級結構平行的Iterator結構,出現了很多的ConcreteIterator類
這勢必會增加維護成本
而且,雖然迭代器將客戶端的訪問與容器進行解耦,但是迭代器卻是必須依賴容器對象的
也就是迭代器類ConcreteIterator與ConcreteAggregate必須進行通信,會增加設計的復雜度,而且這也會增加類之間的耦合性
另外的一種方法是使用內部類的形式,也就是將ConcreteIterator的實現,移入到ConcreteAggregate的內部
借助於內部類的優勢:對外部類有充足的訪問權限,也就是無需擔心為了通信要增加復雜度的問題
准確的說,你沒有任何的通信成本,內部類可以直接讀取外部類的屬性數據信息
而且,使用內部類的方式不會導致類的爆炸(盡管仍舊是會有另一個class文件,但是從代碼維護的角度看算是一個類)
這種形式可以叫做內部迭代器
不過無論哪種方式,你可以看得出來,使用迭代器的客戶端代碼,都是一樣的
借助於工廠方法iterator()獲得一個迭代器實例(簡單工廠模式)
然后借助於迭代器進行元素遍歷
JDK中的迭代
我們看下JDK中的Collection提供給我們的迭代方式
Collection是所有集合的父類,Collection實現了Iterable接口
Iterable接口提供了iterator()方法用於返回一個Iterator類的一個實例對象
Iterator類提供了對元素的遍歷方法
接下來看下ArrayList的實現
ArrayList中iterator()返回了一個Itr對象,而這個對象是ArrayList的內部類,實現了Iterator接口
看得出來,java給集合框架內置了迭代器模式
在ArrayList中使用就是內部類的形式,也就是內部迭代器
boolean hasNext()
是否擁有更多元素,換句話說,如果next()方法不會拋出異常,就會返回true
next();
返回下一個元素
remove()
刪除元素
有幾點需要注意
1.)初始時,可以認為“當前位置”為第一個元素前面
所以next()獲取第一個元素
2.)根據第一點,初始的當前位置”為第一個元素前面,所以如果想要刪除第一個元素的話,必須先next,然后remove
Iterator iterator = list.iterator();
iterator.next();
iterator.remove();
否則,會拋出異常
3.)不僅僅是刪除第一個元素需要先next,然后才能remove,每一個remove,前面必須有一個next,成對出現
所以remove是刪除當前元素
如果下面這樣,會拋出異常
iterator.next();
iterator.remove();
iterator.remove();
4.)迭代器只能遍歷一次,如果需要重新遍歷,可以重新獲取迭代器對象
如果已經遍歷到尾部之后仍舊繼續使用,將會拋出異常
Iterator iterator = list.iterator(); while (iterator.hasNext()) { iterator.next(); } iterator.next();
總結
在java中萬事萬物都是對象
前面的命令模式將請求轉換為命令對象
解釋器模式中,將語法規則轉換為終結符表達式和非終結符表達式
在迭代器模式中,將“遍歷元素”轉換為對象
通過迭代器模式引入迭代器,將遍歷邏輯功能從容器聚集對象中分離出來
聚合對象本身只負責數據存儲,遍歷的職責交給了迭代器
對於同一個容器對象,可以定義多種迭代器,也就是可以定義多種遍歷方式
如果需要使用另外的迭代方式,僅僅需要更改迭代器對象即可
這樣你甚至可以把ConcreteIterator使用配置文件進行注入,靈活設置
將迭代遍歷的邏輯從容器對象中分離,必然會減少容器類的復雜程度
當增加新的容器類或者迭代器類時,不需要修改原有的代碼,符合開閉原則
如果你想要將容器聚集對象的遍歷邏輯從容器對象中的分離
或者想要提供多種不同形式的遍歷方式時,或者你想為不同的容器對象提供一致性的遍歷接口邏輯
你就應該考慮迭代器模式了
迭代器模式的應用是如此廣泛,以至於java已經將他內置到集合框架中了
所以對於我們自己來說,多數時候可以認為迭代器模式幾乎用不到了
因為絕大多數時候,使用框架提供的應該就足夠了
在java實現中,迭代器模式的比較好的做法就是Java集合框架使用的這種形式---內部類形式的內部迭代器,如果真的需要自己搞一個迭代器,建議仿照集合框架搞吧
借助於迭代器模式,如果迭代的邏輯不變,更換另外的集合實現,因為實現了共同的迭代器接口,所以不需要對迭代這塊,無需做任何變動
如果需要改變迭代邏輯,必須增加新的迭代形式,只需要增加一個新的內部類實現迭代器接口即可,其他使用的地方只需要做很小的調整
ArrayList中的ListIterator<E> listIterator() 方法就是如此
有人覺得增加一個類和一個方法這不也是修改么?個人認為:開閉原則盡管最高境界是完全的對擴展開放對修改關閉,但是也不能死摳字眼
增加了一個新的獲取迭代對象的方法以及一個新的類,總比將原有的源代碼中添加新的方法那種修改要強得多,所有的遍歷邏輯都封裝在新的迭代器實現類中,某種程度上可以認為並沒有“修改源代碼”
使用內部類的形式,有人覺得不還是在一個文件中么?但是內部類會有單獨的class文件,而且,內部類就像一道牆,分割了內外,所有的邏輯被封裝在迭代器實現類中
不需要影響容器自身的設計實現,所以也是符合單一職責原則的。