這是我參與8月更文挑戰的第5天,活動詳情查看: 8月更文挑戰java
😪 早上困困,啃下設計模式之美提提神,本文對應設計模式與範式:行爲型(65-67),迭代器模式 (Iterator Pattern),又稱 遊標模式
,用於 解耦容器代碼和遍歷代碼
。算法
不過,不少編程語言都將迭代器做爲一個基礎類庫,直接提供出來了。平常業務開發,不多本身實現一個迭代器,固然,弄懂原理能幫助咱們更好地使用這些工具類~編程
Tips:二手知識加工不免有所紕漏,感興趣有時間的可自行查閱原文,謝謝。設計模式
原始定義數組
迭代器提供一種對容器對象中各個元素進行訪問的方法,而又不須要暴露該對象的內部細節。安全
定義很好理解,上構成該模式的四個角色:markdown
其實就是兩類角色:容器 和 迭代器,寫個簡單示例幫助理解~數據結構
// 歌曲實體
public class Music {
private String name;
private String singer;
private long createTime;
public Music(String name, String singer, long createTime) {
this.name = name;
this.singer = singer;
this.createTime = createTime;
}
public String getName() { return name; }
public String getSinger() { return singer; }
public long getCreateTime() { return createTime; }
@Override
public String toString() {
return "【" + name + "】- " + singer + " - " + createTime;
}
}
// 抽象迭代器
public interface Iterator {
// 最基本的兩個方法
Music next();
boolean hasNext();
// 按需添加
Music currentItem();
Music first();
}
// 抽象容器
public interface Container {
Iterator createIterator();
}
// 具體迭代器
public class ConcreteIterator implements Iterator {
private Music[] musics;
private int pos = 0;
// 待遍歷容器經過依賴注入傳遞到具體迭代器類中
public ConcreteIterator(Music[] musics) { this.musics = musics; }
@Override public Music next() { return musics[pos++]; }
@Override public boolean hasNext() { return pos < musics.length; }
@Override public Music currentItem() { return musics[pos]; }
@Override public Music first() { return musics[0]; }
}
// 具體容器
public class ConcreteContainer implements Container {
private Music[] musics;
public ConcreteContainer(Music[] musics) { this.musics = musics; }
@Override public Iterator createIterator() { return new ConcreteIterator(musics); }
}
// 測試用例
public class IteratorTest {
public static void main(String[] args) {
Music[] musics = new Music[5];
musics[0] = new Music("We Sing. We Dance. We Steal Things.", "Jason Mraz", 20080513);
musics[1] = new Music("Viva La Vida Death And All His Friends", "Coldplay", 20080617);
musics[2] = new Music("華麗的冒險 ", "陳綺貞", 20050923);
musics[3] = new Music("范特西 Fantasy", "周杰倫", 20010914);
musics[4] = new Music("後。青春期的詩 後青春期的詩", "五月天", 20081023);
Container container = new ConcreteContainer(musics);
Iterator iterator = container.createIterator();
while (iterator.hasNext()) {
System.out.println(iterator.currentItem());
iterator.next();
}
}
}
複製代碼
代碼運行結果以下:多線程
代碼很是簡單:併發
你可能或說過分設計了,上面的遍歷操做,本身經過 for循環 或 foreach循環 均可以實現。
的確如此,那爲啥還要給容器設計對應的迭代器呢?三個緣由:
在舉個例子,如今須要按照歌曲時間升序遍歷,只須要實現一個迭代器類:
public class OrderTimeIterator implements Iterator {
private final Music[] musics;
private int pos;
public OrderTimeIterator(Music[] musics) {
this.musics = new Music[musics.length];
System.arraycopy(musics, 0, this.musics, 0, musics.length);
sortByTimeAsc(this.musics, 0, this.musics.length - 1);
this.pos = 0;
}
// 快速排序
private void sortByTimeAsc(Music[] arr, int low, int high) {
if(low > high) return;
int i = low;
int j = high;
Music temp;
Music anchor = arr[low];
while (i < j) {
while (arr[j].getCreateTime() >= anchor.getCreateTime() && i < j) {
j--;
}
while (arr[i].getCreateTime() <= anchor.getCreateTime() && i < j) {
i++;
}
if(i < j) {
temp = arr[j];
arr[j] = arr[i];
arr[i] = temp;
}
}
arr[low] = arr[i];
arr[i] = anchor;
sortByTimeAsc(arr, low, j -1);
sortByTimeAsc(arr, j + 1, high);
}
... // 其餘實現方法同ConcreteIterator
}
複製代碼
具體容器類返回迭代器createIterator()方法,改爲new OrderTimeIterator()便可,輸出結果以下:
不懂快排的童鞋不須要了解具體細節,直接換迭代器便可,還能夠按照本身的需求自定義迭代器,妙啊。
對了foreach循環語法糖,其實也是基於迭代器實現的,接着帶出UML類圖、使用場景和優缺點:
使用場景
優勢
缺點
問題來了 → 當遍歷的同時增刪集合元素會怎麼樣?
答:可能會致使重複遍歷或遍歷不到某個元素。
是可能,並不會全部狀況下都遍歷出錯,有時還能夠正常遍歷,這種行爲稱爲 結果不可預期行爲 或 未決行爲,即運行的結果是對是錯,得是狀況而定。
好比原列表長度爲5,迭代的時候插入了一個元素,但迭代器length仍是以前的5,會漏掉新插入的元素; 又好比迭代時刪掉了最後一個元素,但迭代器length仍是以前的5,會引起數組越界;
如何應對這種遍歷時改變集合致使的未決行爲?
方法一比較難實現,得肯定遍歷開始與結束的時間點,開始好拿(如建立迭代器時),結束很差拿,由於遍歷不必定把全部元素都走一遍,好比找到知足條件的元素,提早結束遍歷。
在迭代器內定義一個接口finishIteration(),主動告知容器迭代器使用完畢,但這就要求調用者在使用完迭代器後要主動調用此函數,增長了開發成本之餘還容易漏掉。
Java語言中採用的方法二,如ArrayList中定義了一個成員變量modCount,記錄集合被修改的次數,調用增刪函數都會加1。
建立迭代器的時候傳入,而後每次調用迭代器的next()、hasNext()函數時都檢查集合中的modCount是否等於一開始傳入的modCount,不等說明集合存儲的元素已經發生改變,以前建立的迭代器已不能正確運行,直接拋出運行時異常,結束程序。
另外,在單線程狀況下,ArrayList使用迭代器進行迭代,經過迭代器增刪元素,不會引起異常,原理是:
內部類Itr 實現Iterator接口,定義了兩個變量cursor (下一個元素下標) 和 lastRet (上一個元素下標) 當發生元素增刪時,更新迭代器中的遊標及這兩個值,保證遍歷不出錯。
而對於多線程的狀況,除了在iterator使用處加鎖外,還能夠用 併發容器。
原理是:採用的是 fail-safe(安全失敗) 機制:迭代時不是直接在集合內容上訪問的,而是先複製原有集合內容,在拷貝的集合上進行遍歷。因此遍歷期間原集合發生的修改迭代器是不知道的,原迭代器也不能訪問修改後的內容。Java的併發容器放在java.util.concurrent包中,如使用 CopyOnWriteArrayList 來代替ArrayList。
所謂的 "快照" 就是建立迭代器時至關於給容器拍了張快照(Snapshot),以後增刪容器元素,快照中的元素都不會發生改變,即迭代器遍歷的對象是快照而非容器。經過一個例子來解釋這段話:
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
ListIterator<String> it1 = list.listIterator();
while (it1.hasNext()) System.out.print(it1.next()); // 輸出: abcd
System.out.println();
list.remove("a");
ListIterator<String> it2 = list.listIterator();
while (it2.hasNext()) System.out.print(it2.next()); // 輸出:bcd
System.out.println();
list.remove("c");
ListIterator<String> it3 = list.listIterator();
while (it3.hasNext()) System.out.print(it3.next()); // 輸出:bd
複製代碼
第一種解法:迭代器類中定義一個存儲快照的成員變量,構造迭代器時複製原集合引用進行初始化,後續遍歷都基於持有的快照進行。(我上面定義的OrderTimeIterator就是這種)。
固然,缺點明顯,每次建立迭代器,都要拷貝一份數據到快照中,增長內存消耗,當有多個迭代器在遍歷元素,還會致使重複存儲多份。不過,好在Java中的拷貝屬於淺拷貝,因此只是拷貝了對象的引用而已。
第二種解法:容器中爲每一個元素保存兩個時間戳,添加時間 及 刪除時間 (初始化爲最大長整型值),添加時將添加時間設置爲當前時間,刪除時將時間設置爲當前時間,記住只是 標記刪除,並不是真的從容器中將其刪除。
而後每一個迭代器保存一個 建立時間,即快照建立時間戳,當使用迭代器遍歷容器時,只有知足:
添加時間 < 建立時間 < 刪除時間
的元素才屬於這個迭代器的快照:
- 添加時間 > 建立時間 → 說明元素在建立迭代器後才加入,不屬於這個迭代器的快照;
- 刪除時間 < 建立事件 → 說明元素在建立迭代器前就被刪除了,一樣不屬於這個迭代器的快照;
在不拷貝容器的狀況下,在容器自己藉助時間戳實現快照功能,妙啊!
這種方式解決了一個問題,又引入了一個問題:
ArrayList底層依賴數組這種存儲結構,本來支持快速隨機訪問,在O(1)時間複雜度內獲取下標爲i的元素。但如今刪除元素並無真正刪除,這就致使沒法支持按照下標快速隨機訪問了。
解法:
在ArrayList中存儲兩個數組,一個支持標記刪除,用來實現快照遍歷,一個不支持標記刪除(刪除數據直接從數組中刪除),用來支持隨機訪問。
以上內容就是本節的所有內容,謝謝~