HashMap 簡介
HashMap
是一個基於雜湊表實現的無序的key-value
容器,它鍵和值允許設定為 null
,同時它是執行緒不安全的。
HashMap 底層實現
- 在
jdk 1.7
中HashMap
是以陣列+連結串列的實現的 - 在
jdk1.8
開始引入紅黑樹,HashMap
底層變成了陣列+連結串列+紅黑樹實現
紅黑樹簡介
紅黑樹是一種特殊的平衡二叉樹,它有如下的特徵:
- 節點是紅色或黑色
- 根節點是黑色的
- 所有葉子都是黑色。(葉子是
NULL
節點) - 每個紅色節點的兩個子節點都是黑色的(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)
- 從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。
所以紅黑樹的時間複雜度為: O(lgn)
。
jdk1.8:陣列+連結串列+紅黑樹
HashMap
的底層首先是一個陣列,元素存放的陣列索引值就是由該元素的雜湊值(key-value
中key
的雜湊值)確定的,這就可能產生一種特殊情況——不同的key
雜湊值相同。
在這樣的情況下,於是引入連結串列,如果key
的雜湊值相同,在陣列的該索引中存放一個連結串列,這個連結串列就包含了所有key
的雜湊值相同的value
值,這就解決了雜湊衝突的問題。
但是如果發生大量雜湊值相同的特殊情況,導致連結串列很長,就會嚴重影響HashMap
的效能,因為連結串列的查詢效率需要遍歷所有節點。於是在jdk1.8
引入了紅黑樹,當連結串列的長度大於8,且HashMap
的容量大於64的時候,就會將連結串列轉化為紅黑樹。
// jdk1.8
// HashMap#putVal
// binCount 是該連結串列的長度計數器,當連結串列長度大於等於8時,執行樹化方法
// TREEIFY_THRESHOLD = 8
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
// HashMap#treeifyBin
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// MIN_TREEIFY_CAPACITY=64
// 若 HashMap 的大小小於64,僅擴容,不樹化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
載入因子為什麼是0.75
所謂的載入因子,也叫擴容因子或者負載因子,它是用來進行擴容判斷的。
假設載入因子是0.5,HashMap
初始化容量是16,當HashMap
中有16 * 0.5=8
個元素時,HashMap
就會進行擴容操作。
而HashMap
中載入因子為0.75,是考慮到了效能和容量的平衡。
由載入因子的定義,可以知道它的取值範圍是(0, 1]。
- 如果載入因子過小,那麼擴容門檻低,擴容頻繁,這雖然能使元素儲存得更稀疏,有效避免了雜湊衝突發生,同時操作效能較高,但是會佔用更多的空間。
- 如果載入因子過大,那麼擴容門檻高,擴容不頻繁,雖然佔用的空間降低了,但是這會導致元素儲存密集,發生雜湊衝突的概率大大提高,從而導致儲存元素的資料結構更加複雜(用於解決雜湊衝突),最終導致操作效能降低。
- 還有一個因素是為了提升擴容效率。因為
HashMap
的容量(size
屬性,建構函式中的initialCapacity
變數)有一個要求:它一定是2的冪。所以載入因子選擇了0.75就可以保證它與容量的乘積為整數。
// 建構函式
public HashMap(int initialCapacity, float loadFactor) {
// ……
this.loadFactor = loadFactor;// 載入因子
this.threshold = tableSizeFor(initialCapacity);
}
/**
* Returns a power of two size for the given target capacity.返回2的冪
* MAXIMUM_CAPACITY = 1 << 30
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
HashMap 的容量為什麼是2的 n 次冪
HashMap
的預設初始容量是16,而每次擴容是擴容為原來的2倍。這裡的16和2倍就保證了HashMap
的容量是2的n次冪,那麼這樣設計的原因是什麼呢?
原因一:與運算高效
與運算&
,基於二進位制數值,同時為1結果為1,否則就是0。如1&1=1,1&0=0,0&0=0。使用與運算的原因就是對於計算機來說,與運算十分高效。
原因二:有利於元素充分雜湊,減少 Hash 碰撞
在給HashMap
新增元素的putVal
函式中,有這樣一段程式碼:
// n為容量,hash為該元素的hash值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
它會在新增元素時,通過i = (n - 1) & hash
計算該元素在HashMap
中的位置。
當 HashMap 的容量為 2 的 n 次冪時,他的二進位制值是100000……(n個0),所以n-1的值就是011111……(n個1),這樣的話(n - 1) & hash
的值才能夠充分雜湊。
舉個例子,假設容量為16,現在有雜湊值為1111,1110,1011,1001四種將被新增,它們與n-1(15的二進位制=01111)的雜湊值分別為1111、1110、1110、1011,都不相同。
而假設容量不為2的n次冪,假設為10,那麼它與上述四個雜湊值進行與運算的結果分別是:0101、0100、0001、0001。
可以看到後兩個值發生了碰撞,從中可以看出,非2的n次冪會加大雜湊碰撞的概率。所以 HashMap 的容量設定為2的n次冪有利於元素的充分雜湊。
參考:HashMap初始容量為什麼是2的n次冪及擴容為什麼是2倍的形式
HashMap 是如何導致死迴圈的
HashMap
會導致死迴圈是在jdk1.7
中,由於擴容時的操作是使用頭插法,在多執行緒的環境下可能產生迴圈連結串列,由此導致了死迴圈。在jdk1.8
中改為使用尾插法,避免了該死迴圈的情況。
在網上找到了比較詳細的解釋分析部落格與視訊: