一、前言
上面這幅圖是Java
集合框架涉及到的類的繼承關係,從集合類的角度來看,它分為兩個大類:Collection
和Map
。
1.1 Collection
Collection
是List
和Set
抽象出來的介面,它包含了這些集合的基本操作。
(1) List
List
介面通常表示一個列表(陣列、佇列、連結串列,棧等),其中的元素可以重複,常用的實現類為ArrayList
、LinkedList
和Vector
。
(2) Set
Set
介面通常表示一個集合,集合中的元素不允許重複(通過hashCode
和equals
函式保證),常用的實現類有HashSet
和TreeSet
,HashSet
是通過Map
中的HashMap
來實現的,而TreeSet
則是通過Map
中的TreeMap
實現的,另外TreeSet
還實現了SortedSet
介面,因此是有序的集合。
(3) List 和 Set 的區別
Set
介面儲存的是無序的、不重複的資料List
介面儲存的是有序的、可以重複的資料Set
檢索效率低,刪除和插入效率高,插入和刪除不會引起元素位置改變。List
查詢元素效率高,刪除和插入效率低,List
和陣列類似,可以動態增長,根據實際儲存的長度自動增長List
的長度。
(4) 使用的設計模式
抽象類AbstractCollection
、AbstractList
和AbstractSet
分別實現了Collection
、List
和Set
介面,這就是在Java
集合框架中用的很多的介面卡設計模式,用這些抽象類去實現介面,在抽象類中實現介面中的若干或全部方法,這樣下面的一些類只需直接繼承該抽象類,並實現自己需要的方法即可,而不用實現介面中的全部抽象方法。
1.2 Map
Map
是一個對映介面,其中的每個元素都是一個Key-Value
鍵值對,同樣抽象類AbstractMap
通過介面卡模式實現了Map
介面的大部分函式,TreeMap
、HashMap
和WeakHashMap
等實現類都通過繼承AbstractMap
來實現。
1.3 Iterator
Iterator
是遍歷集合的迭代器,它可以用來遍歷Collection
,但是不能用來遍歷Map
。Collection
的實現類都實現了iterator()
函式,它返回一個Iterator
物件,用來遍歷集合,ListIterator
則專門用來遍歷List
。而Enumeration
則是JDK 1.0
時引入的,作用與Iterator
相同,但它的功能比Iterator
要少,它只能在Hashtable
、Vector
和Stack
中使用。
1.4 Arrays 和 Collections
Arrays
和Collections
是用來運算元組、集合的兩個工具類,例如在ArrayList
和Vector
中大量呼叫了Arrays.Copyof()
方法,而Collections
中有很多靜態方法可以返回各集合類的synchronized
版本,即執行緒安全的版本,當然了,如果要用執行緒安全的集合類,首選concurrent
併發包下的對應的集合類。
二、ArrayList
ArrayList
是基於一個能動態增長的陣列實現,ArrayList
並不是執行緒安全的,在多執行緒的情況下可以考慮使用Collections.synchronizedList(List T)
函式返回一個執行緒安全的ArrayList
類,也可以使用併發包下的CopyOnWriteArrayList
類。
ArrayList<T>
類繼承於AbstractList<T>
,並實現了以下四個介面:
List<T>
RandomAccess
:支援快速隨機訪問Cloneable
:能夠被克隆Serializable
:支援序列化
ArrayList 的擴容
由於ArrayList
是基於陣列實現的,因此當我們通過addXX
方法向陣列中新增元素之前,都要保證有足夠的空間容納新的元素,這一過程是通過ensureCapacityInternal
來實現的,傳入的引數為所要求的陣列容量:
- 如果當前陣列為空,並且要求的容量小於
10
,那麼將要求的容量設為10
- 接著嘗試將陣列大小擴充為當前大小的
2.5
倍 - 如果仍然無法滿足要求,那麼將陣列大小設為要求的容量
- 如果要求的容量大於預設的整型的最大值減
8
,那麼呼叫hugeCapacity
方法,將陣列的容量設為整型的最大值 - 最後,呼叫
Arrays.copyOf
將原有陣列中的元素複製到新的陣列中。
Arrays.copyOf
最終會呼叫到System.arraycopy()
方法。該Native
函式實際上最終呼叫了C
語言的memmove()
函式,因此它可以保證同一個陣列內元素的正確複製和移動,比一般的複製方法的實現效率要高很多,很適合用來批量處理陣列,Java
強烈推薦在複製大量陣列元素時用該方法,以取得更高的效率。
ArrayList 轉換為靜態陣列
ArrayList
中提供了兩種轉換為靜態陣列的方法:
Object[] toArray()
該方法有可能會丟擲java.lang.ClassCastException
異常,如果直接用向下轉型的方法,將整個ArrayList
集合轉變為指定型別的Array
陣列,便會丟擲該異常,而如果轉化為Array
陣列時不向下轉型,而是將每個元素向下轉型,則不會丟擲該異常,顯然對陣列中的元素一個個進行向下轉型,效率不高,且不太方便。T[] toArray(T[] a)
該方法可以直接將ArrayList
轉換得到的Array
進行整體向下轉型,且從該方法的原始碼中可以看出,引數a
的大小不足時,內部會呼叫Arrays.copyOf
方法,該方法內部建立一個新的陣列返回,因此對該方法的常用形式如下:
public static Integer[] vectorToArray2(ArrayList<Integer> v) {
Integer[] newText = (Integer[])v.toArray(new Integer[0]);
return newText;
}
複製程式碼
元素訪問方式
ArrayList
基於陣列實現,可以通過下標索引直接查詢到指定位置的元素,因此查詢效率高,但每次插入或刪除元素,就要大量地移動元素,插入刪除元素的效率低。
在查詢給定元素索引值等的方法中,原始碼都將該元素的值分為null
和不為null
兩種情況處理,ArrayList
中允許元素為null
。
三、LinkedList
LinkedList
是基於雙向迴圈連結串列實現的,除了可以當作連結串列來操作外,它還可以當作棧,佇列和雙端佇列來使用。
LinkedList
同樣是非執行緒安全的,在多執行緒的情況下可以考慮使用Collections.synchronizedList(List T)
函式返回一個執行緒安全的LinkedList
類,LinkedList
繼承於AbstractSequentialList
類,同時實現了以下四個介面:
List<T>
Deque
和Queue
:雙端佇列Cloneable
:支援克隆操作Serializable
:支援序列化
連結串列節點
LinkedList的
實現是基於雙向迴圈連結串列的,且頭結點voidLink
中不存放資料,所以它也不存在擴容的方法,只需改變節點的指向即可,每個連結串列節點包含該節點的資料,以及前驅和後繼節點的引用,其定義如下所示:
private static final class Link<ET> {
//該節點的資料。
ET data;
//前驅節點和後繼節點。
Link<ET> previous, next;
Link(ET o, Link<ET> p, Link<ET> n) {
data = o;
previous = p;
next = n;
}
}
複製程式碼
查詢和刪除操作
當需要根據位置尋找對應節點的資料時,會先比較待查詢位置和連結串列的大小,如果小於一半,那麼從頭節點的後繼節點開始向後尋找,反之則從頭結點的前驅節點開始往前尋找,因此對於查詢操作來說,它的效率很低,但是向頭尾節點插入和刪除資料的效率較高。
四、Vector
Vector
也是基於陣列實現的,其容量能夠動態增長。它的許多實現方法都加入了同步語句,因此是 執行緒安全 的。
Vector
繼承於AbstractList
類,並且實現了下面四個介面:
List<E>
RandomAccess
:支援隨機訪問Cloneable, java.io.Serializable
:支援Clone
和序列化。
Vector
的實現大體和ArrayList
類似,它有以下幾個特點:
Vector
有四個不同的構造方法,無參構造方法的容量為預設值10
,僅包含容量的構造方法則將容量增長量置為0
。- 當
Vector
的容量不足以容納新的元素時,將進行擴容操作。首先判斷容量增長值是否為0
,如果為0
,那麼就將新容量設為舊容量的兩倍,否則就設定新容量為舊容量加上容量增長值。假如新容量還不夠,那麼就直接設定新量容量為傳入的引數。 - 在存入和讀取元素時,會根據元素值是否為
null
進行處理,也就是說,Vector
允許元素為null
。
五、HashSet
HashSet
具有以下特點:
- 不能保證元素的排列順序,順序有可能發生變化
- 不是同步的
- 集合元素可以是
null
,但只能放入一個null
當向HashSet
集合中存入一個元素時,HashSet
會呼叫該物件的hashCode()
方法來得到該物件的hashCode
值,然後根據hashCode
值來決定該物件在HashSet
中儲存位置。
簡單的說,HashSet
集合判斷兩個元素相等的標準是兩個物件通過equals
方法比較相等,並且兩個物件的hashCode()
方法返回值相等。
注意,如果要把一個物件放入HashSet
中,重寫該物件對應類的equals
方法,也應該重寫其hashCode()
方法。其規則是如果兩個物件通過equals
方法比較返回true
時,其hashCode
也應該相同。另外,物件中用作equals
比較標準的屬性,都應該用來計算hashCode
的值。
六、TreeSet
TreeSet
是SortedSet
介面的唯一實現類,TreeSet
可以確保集合元素處於排序狀態。TreeSet
支援兩種排序方式,自然排序 和 定製排序,其中自然排序為預設的排序方式。
向TreeSet
中加入的應該是同一個類的物件。TreeSet
判斷兩個物件不相等的方式是兩個物件通過equals
方法返回false
,或者通過CompareTo
方法比較沒有返回0
。
自然排序
自然排序使用要排序元素的CompareTo(Object obj)
方法來比較元素之間大小關係,然後將元素按照升序排列。
Java
提供了一個Comparable
介面,該介面裡定義了一個compareTo(Object obj)
方法,該方法返回一個整數值,實現了該介面的物件就可以比較大小。
obj1.compareTo(obj2)
方法如果返回0
,則說明被比較的兩個物件相等,如果返回一個正數,則表明obj1
大於obj2
,如果是負數,則表明obj1
小於obj2
。如果我們將兩個物件的equals
方法總是返回true
,則這兩個物件的compareTo
方法返回應該返回0
.
定製排序
自然排序是根據集合元素的大小,以升序排列,如果要定製排序,應該使用Comparator
介面,實現int compare(T o1,T o2)
方法。
TreeSet
是二叉樹實現的,Treeset
中的資料是自動排好序的,不允許放入null
值。HashSet
是雜湊表實現的,HashSet中
的資料是無序的,可以放入null
,但只能放入一個null
,兩者中的值都不能重複,就如資料庫中唯一約束。HashSet
要求放入的物件必須實現hashCode()
方法,放入的物件,是以hashcode()
碼作為標識的,而具有相同內容的String
物件,hashcode
是一樣,所以放入的內容不能重複。但是同一個類的物件可以放入不同的例項 。
七、HashMap
HashMap
是基於雜湊表實現的,每一個元素都是一個key-value
對,其內部通過單連結串列解決衝突問題,容量不足時,同樣會自動增長。HashMap
是非執行緒安全的,只是用於單執行緒環境下,多執行緒環境下可以採用併發包下的ConcurrentHashMap
。
HashMap
繼承於AbstractMap
,同時實現了Cloneable
和Serializable
介面,因此,它支援克隆和序列化。
HashMap 的整體結構
HashMap
是基於陣列和連結串列來實現的:
- 首先根據
Key
的hashCode
方法,計算出在陣列中的座標。
//計算 Key 的 hash 值。
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
//根據 Key 的 hash 值和連結串列的長度來計算下標。
int i = indexFor(hash, table.length);
複製程式碼
- 判斷在陣列的當前位置是否已經有元素,如果沒有,那麼就將
Key/Value
封裝成HashMapEntry
資料結構,並將其作為陣列在該位置上的元素。否則就先從頭節點開始遍歷該連結串列,如果 滿足下面的兩個條件,那麼就替換連結串列該節點的Value
//Value 替換的條件
//條件1:hash 值完全相同
//條件2:key 指向同一塊記憶體地址 或者 key 的 equals 方法返回為 true
(e.hash == hash && ((k = e.key) == key || key.equals(k)))
複製程式碼
- 遍歷完整個連結串列都沒有找到可替代的節點,那麼將這個新的
HashMapEntry
作為連結串列的頭節點,並且也是陣列在該位置上的元素,原先的頭節點則作為它的後繼節點。
HashMapEntry 的資料結構
HashMapEntry
的定義如下:
static class HashMapEntry<K,V> implements Map.Entry<K,V> {
//Key
final K key;
//Value
V value;
//後繼節點。
HashMapEntry<K,V> next;
//如果 Key 不為 null ,那麼就是它的雜湊值,否則為0。
int hash;
//....
}
複製程式碼
元素寫入
在第一小節中,我們簡要的計算了HashMap
的整體結構,由此我們可以推斷出在設計的時候應當儘可能地使元素均勻分佈,使得陣列每個位置上的連結串列儘可能地短,避免從連結串列頭結點開始遍歷的過程。
而元素是否分佈均勻就取決於根據Key
的Hash
值計算陣列下標的過程,首先我們看一下Hash
值的計算,這裡首先呼叫物件的hashCode
方法,再通過二次Hash
演算法獲得一個Hash
值:
public static int secondaryHash(Object key) {
return secondaryHash(key.hashCode());
}
private static int secondaryHash(int h) {
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
複製程式碼
之後,再通過這個計算出來Hash
值 與上當前陣列長度減一 進行取餘,獲得對應的陣列下標:
hash & (tab.length - 1)
複製程式碼
由於HashMap
在擴容的時候,保證了陣列的長度適中為2
的n
冪,因此length - 1
的二進位制表示始終為全1
,因此進行&
操作的結果既保證了最終的結果不會超過陣列的長度範圍,同時也保證了兩個Hash
值相同的元素不會對映到陣列的同一位置,再加上上面二次Hash
的過程加上了高位的計算優化,從而使得資料的分佈儘可能地平均。
元素讀取
理解了上面儲存的過程,讀取自然也就很好理解了,其實通過Key
計算陣列下標,遍歷該位置上陣列元素的連結串列進行查詢的過程。
擴容
當HashMap
中的元素越來越多的時候,hash
衝突的機率也就越來越高,因為陣列的長度是固定的,所以為了提高查詢的效率,就要對HashMap
的陣列進行擴容。
當HashMap
中的元素個數超過陣列大小 * loadFactor
時,loadFactor
的預設值為0.75,就會進行陣列擴容,擴容後的大小為原先的2
倍,然後重新計算每個元素在陣列中的位置,原陣列中的資料必須重新計算其在新陣列中的位置,並放進去。
擴容是一個相當耗費效能的操作,因此如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提高HashMap
的效能。
Fail-Fast 機制
HashMap
並不是執行緒安全的,因此如果在使用迭代器的過程中有其他執行緒修改了map
,那麼將丟擲ConcurrentModificationException
,這就是所謂fail-fast
策略。
這一策略在原始碼中的實現是通過modCount
域,modCount
顧名思義就是修改次數,對HashMap
內容的修改都將增加這個值,那麼在迭代器初始化過程中會將這個值賦給迭代器的expectedModCount
。
在迭代過程中,判斷modCount
跟expectedModCount
是否相等,如果不相等就表示已經有其他執行緒修改了Map
,那麼就會通過下面的方法丟擲異常:
HashMapEntry<K, V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
//省略...
}
複製程式碼
modCount
宣告為volatile
,保證了多執行緒情況下的記憶體可見性。
在迭代器建立之後,如果從結構上對對映進行修改,除非通過迭代器本身的remove
方法,其他任何時間任何方式的修改,迭代器都將丟擲ConcurrentModificationException
。因此,面對併發的修改,迭代器很快就會完全失敗,而不保證在將來不確定的時間發生任意不確定行為的風險
八、HashTable
HashTable
經常用來和HashMap
進行比較,前者是執行緒安全的,而後者則不是,其實HashTable
要比HashMap
出現得要早,它實現執行緒安全的原理並沒有什麼高階的地方,只不過是在寫入和讀取時加上了synchronized
關鍵字用於同步,並且也不推薦使用了,因為在併發包中提供了更好的解決方案ConcurrentHashMap
,它內部的實現比較複雜,之後我們再通過一篇文章進行分析。
這裡簡單地總結一下它和HashMap
之間的區別:
HashTable
基於Dictionary
類,而HashMap
是基於AbstractMap
。Dictionary
是任何可將鍵對映到相應值的類的抽象父類,而AbstractMap
基於Map
介面的實現,它以最大限度地減少實現此介面所需的工作。HashMap
的key
和value
都允許為null
,而Hashtable
的key
和value
都不允許為null
。HashMap
遇到key
為null
的時候,呼叫putForNullKey
方法進行處理,而對value
沒有處理,Hashtable
遇到null
,直接返回NullPointerException
。Hashtable
方法是同步,而HashMap
則不是。我們可以看一下原始碼,Hashtable
中的幾乎所有的public
的方法都是synchronized
的,而有些方法也是在內部通過synchronized
程式碼塊來實現。所以有人一般都建議如果是涉及到多執行緒同步時採用HashTable
,沒有涉及就採用HashMap
,但是在Collections
類中存在一個靜態方法:synchronizedMap()
,該方法建立了一個執行緒安全的Map
物件,並把它作為一個封裝的物件來返回。
九、TreeMap
TreeMap
是一個有序的key-value
集合,它是通過 紅黑樹 實現的。TreeMap
繼承於AbstractMap
,所以它是一個Map
,即一個key-value
集合。TreeMap
實現了NavigableMap
介面,意味著它支援一系列的導航方法,比如返回有序的key集合。TreeMap
實現了Cloneable
和Serializable
介面,意味著它可以被Clone
和序列化。
TreeMap
基於紅黑樹實現,該對映根據其鍵的自然順序進行排序,或者根據建立對映時提供的 Comparator
進行排序,具體取決於使用的構造方法。TreeMap
的基本操作containsKey
、get
、put
和remove
的時間複雜度是log(n)
,另外,TreeMap
是非同步的, 它的iterator
方法返回的迭代器是Fail-Fastl
的。
十、LinkedHashMap
LinkedHashMap
是HashMap
的子類,與HashMap
有著同樣的儲存結構,但它加入了一個雙向連結串列的頭結點,將所有put
到LinkedHashmap
的節點一一串成了一個雙向迴圈連結串列,因此它保留了節點插入的順序,可以使節點的輸出順序與輸入順序相同。LinkedHashMap
可以用來實現LRU
演算法。LinkedHashMap
同樣是非執行緒安全的,只在單執行緒環境下使用。
十一、LinkedHashSet
LinkedHashSet
是具有可預知迭代順序的Set
介面的雜湊表和連結列表實現。此實現與HashSet
的不同之處在於,後者維護著一個執行於所有條目的雙重連結列表。此連結列表定義了迭代順序,該迭代順序可為插入順序或是訪問順序。
LinkedHashSet
的實現:對於LinkedHashSet
而言,它繼承與HashSet
、又基於LinkedHashMap
來實現的。