首先,在探索集合之前,我們先來思考一個問題,集合是什麼?
針對一個特定的問題,如果事先不知道需要多少個物件,或者它們的持續時間有多長,那麼也不知道如何儲存那些物件。既然如此,怎樣才能知道那些物件要求多說空間呢?事先上根本無法提前知道,除非進入執行期。
在物件導向的設計中,大多數問題的解決辦法似乎都有些輕率——只是簡單地建立另一種型別的物件。用於解決特定問題的新型物件容納了指向其他物件的引用。當然,也可以用陣列來做同樣的事情,那是大多數語言都具有的一種功能。 但不能只看到這一點。這種新物件通常叫作“集合”(亦叫作一個“容器”)。在 需要的時候,集合會自動擴充自己,以便適應我們在其中置入的任何東西。所以 我們事先不必知道要在一個集合裡容下多少東西。只需建立一個集合,以後的工作讓它自己負責好了。
上文摘抄自《Thinking in Java》,集合解決的問題是,在編譯期間不知道要多少個物件,但是陣列必須在申明的時候明確指明陣列長度,如果食用陣列,申請太多的空間就會造成資源浪費,如果申請太少空間,就不夠用。所以引出了一個概念叫“容器”,來解決這個問題,這個容器就是我們今天要研究的物件--“集合”。
我們先來看一下類關係圖~
Java 提供的集合都在 Java.utils 包下,集合主要分兩類,Collection 和 Map。我們用到的各種型別的集合,都是實現自這兩個介面。集合的實現類有很多,開發過程中,我們需要根據不同的需求,選擇合適的集合設計,以便高效率的解決我們的實際問題。至於什麼場景用哪一種型別的容器,使用這種容器能帶來哪些好處,這就是我們要研究的核心點,也是我們用好 Java 集合的精髓。
磨刀不誤砍柴工,我們在探索集合的架構設計之前,我們先來研究一下Iterator。
Iterator
Iterator :[計]迭代器,迭代程式
迭代器,這裡用到的就是設計模式中的迭代器模式。
迭代器模式
定義:提供一種方法訪問一個容器物件中各個元素,而又不暴露該物件的內部細節。
這裡我們的重點不是迭代器模式,對“迭代器模式”感興趣的童鞋可以自行去了解一波。
先來看看介面 Iterator 的設計。
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> var1) {
Objects.requireNonNull(var1);
while(this.hasNext()) {
var1.accept(this.next());
}
}
}複製程式碼
一共四個方法,其中hasNext()和 next()方法是迭代必須方法。remove()和forEachRemaining()方法有預設實現,小夥伴不要糾結介面怎麼會有預設實現方法,這是 Java 8 的新特性。
- hasNext():是否有下一個元素
- next():獲取下一個元素
- remove():刪除當前元素,非必須的方法,有需要可重寫實現。
- forEachRemaining():給剩下來所有元素做了一個自定義的相同操作。非必須的方法,有需要可重寫實現。
#fail-fast 與 ConcurrentModificationException
fail-fast:是java集合(Collection)中的一種錯誤機制。當多個執行緒對同一個集合的內容進行操作時,就可能會產生fail-fast事件。
ConcurrentModificationException:出現 fail-fast 問題的時候就會丟擲這個異常。
可能問題描述得有點抽象,我舉個例子:假設有個 ArrayList 集合A,A裡面包含10個元素,分別是0~9。假設執行緒a在獲取第5個元素的過程中,執行緒b操作A刪除了第一個元素。那麼問題來了,此時a執行緒是獲取的到結果是5,但是我的本意應該是取到結果4,此時程式發生了錯誤,因此產生 fail-fast 問題,遂丟擲異常。
###解決方案
- 在遍歷過程中所有涉及到改變modCount值得地方全部加上synchronized。
- 用 CopyOnWriteArrayList,ConcurrentHashMap 替換 ArrayList, HashMap,它們的功能和名字一樣,在寫入時會建立一個 copy,然後在這個 copy 版本上進行修改操作,這樣就不會影響原來的迭代。不過壞處就是浪費記憶體。
Iterator 實現迭代功能
Iterator 的實現類一般以內部類的形式寫在集合類裡面。功能的實現是根據各種集合實現的特定實現,比如說 ArrayList 和 LinkedArrayList 的資料結構不一樣,所以 Iterator 實現也不一樣。
這裡以 ArrayList 的 Iterator 舉例子講一下 Iterator 的程式碼實現。
在看原始碼之前,我們先來回顧一下 Iterator 的使用。
不使用 Iterator 遍歷集合是這樣的:
for(int i=0; i<list.size();i++){
// ...
}
//使用 Iterator 遍歷集合是這樣的:
Iterator iterator = list.iterator();
while (iterator.hasNext()){
Object obj = iterator.next()
}複製程式碼
一般情況,如果只是遍歷獲取集合的所有元素,我選擇使用第一種方式,因為用 iterator 感覺好麻煩的樣子。但是肯定很多童鞋都犯過一個這樣的錯誤,我們在 for 迴圈裡面對集合進行了remove操作,但是最後的結果和我們期望的不一樣,這時候老手告訴你,集合不能這樣操作,如果你要remove,請用 iterator操作,那樣不會出問題,於是,我們默默的記下了這個結論。稍後,我們會在Iterator 的原始碼裡面找到原因。
private class Itr implements Iterator<E> {
int cursor;//當前角標位置
int lastRet;//用來標記當前需要刪除的元素角標
int expectedModCount;//ArrayList元素個數
private Itr() {
this.lastRet = -1;//角標預設指向-1
this.expectedModCount = ArrayList.this.modCount;
}
public E next() {
this.checkForComodification();//檢查 fail-fast
int var1 = this.cursor;
if(var1 >= ArrayList.this.size) {
throw new NoSuchElementException();//角標越界
} else {
Object[] var2 = ArrayList.this.elementData;//ArrayList的底層實現實際上就是一個陣列
if(var1 >= var2.length) {//double check
throw new ConcurrentModificationException();
} else {
this.cursor = var1 + 1;
return var2[this.lastRet = var1];
}
}
}
public boolean hasNext() {
//檢查是否大於陣列長度
return this.cursor != ArrayList.this.size;
}
public void remove() {
if(this.lastRet < 0) {
throw new IllegalStateException();
} else {
this.checkForComodification();
try {
ArrayList.this.remove(this.lastRet);//呼叫ArrayList的remove
this.cursor = this.lastRet;//cursor
this.lastRet = -1;//避免同時呼叫兩次remove
this.expectedModCount = ArrayList.this.modCount;//更新陣列長度
} catch (IndexOutOfBoundsException var2) {
throw new ConcurrentModificationException();
}
}
}
}複製程式碼
注視我都寫在程式碼裡面了,其實ArrayList.Iterator 就是一個對陣列的遍歷,較之直接 for()迴圈ArrayList,優點是做了 fail-fast 檢查,並且增加了在遍歷過程中刪除的功能。
再來詳細講一下for()迴圈裡面不能用list.remove(i)的原因。因為在 for(int i=0; i<list.size();i++) 語句中,假設 list 有4個元素,假如果在 i = 0 的時候呼叫了list.remove(i),此時就出現了取值錯位並且漏值的情況。但是在Iterator 裡面,我們可以看到有一行這樣的程式碼 this.cursor = this.lastRet 改變了當前陣列的角標。
這裡分享一個使用for迴圈然後再在迴圈裡面刪除值並且不會出錯的辦法
for (int i = list.size()-1; i >=0; i--) {
String s = list.get(i);
if (s.equals("remove")) {
list.remove(i);
}
}複製程式碼
好了,很簡單的邏輯,Iterator 就分享到這裡吧,不同的集合裡面的 Iterator 的實現方式不一樣,但是邏輯都是一樣的,所以就不再贅述了。
古耐~