一、前言
上面這幅圖是
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
數組,便會拋出該異常。
public class BasicModel {
public static void main(String[] args) {
List<Parent> list = new ArrayList<>();
list.add(new Parent());
//會拋出異常。
Parent[] arrays = (Parent[]) list.toArray();
}
public static class Parent {}
}
而如果轉化為Array
數組時不向下轉型,而是將每個元素向下轉型,則不會拋出該異常,顯然對數組中的元素一個個進行向下轉型,效率不高,且不太方便。
-
T[] toArray(T[] a)
該方法可以直接將ArrayList
轉換得到的Array
進行整體向下轉型,且從該方法的源碼中可以看出,參數a
的大小不足時,內部會調用Arrays.copyOf
方法,該方法內部創建一個新的數組返回,因此對該方法的常用形式如下:
public class BasicModel {
public static void main(String[] args) {
List<Parent> list = new ArrayList<>();
list.add(new Parent());
//不會拋出異常。
Parent[] arrays = (Parent[]) list.toArray(new Parent[0]);
}
public static class Parent {}
}
元素訪問方式
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
來實現的。