1.8版本之前的HashMap結構也沒仔細看過,趁着有時間把1.8版本HashMap結構梳理了一遍。下面就是幾個總結。
HashMap底層就是藉助於數組+鏈表+紅黑樹實現的。
其中紅黑樹是1.8中引入的,引入的目的就是防止數組中單個節點的鏈表過長,導致查詢速度慢,通過藉助於紅黑樹(自平衡二叉樹)良好的查詢速度,提升查詢性能。
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
* 這裏就是實際存放元素的地方
*/
transient Node<K,V>[] table;
兩個核心參數,capacity和loadFactor。
capacity就是指上面table的長度,loadFactor指的是負載因子,主要是用來控制擴容的參數。
初始化HashMap是可以指定capacity的,如果知道大概有多少元素,可以實例化時就指定capacity,免得進行一次次擴容,影響性能,當然也防止沒有什麼元素,但採用默認容量(16),浪費空間;
loadFactor比較重要,默認值0.75f,爲什麼呢?太小了話會導致擴容頻繁,太大了的話,會導致hash衝突嚴重。
什麼時候進行擴容?
當元素的個數size > capacity * loadFactor時就會進行擴容。插入元素時觸發擴容操作。
擴容方法定義:
final Node<K,V>[] resize()
可以看看那些方法會調用resize()
從上面看看putVal方法中resize方法的調用。
//第一次可能調用的地方,如果hashMap的table數組還沒有進行初始化時會調用
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//第二次調用,發現map中元素的個數超過閾值後,會記性擴容
// threshold = capacity * loadFactor
if (++size > threshold)
resize();
從上面截圖可以看到,這個方法都是在調用插入的時候調用的,因爲插入元素纔會導致元素增長,此時就需要考慮擴容了。
什麼時候進行鏈表和紅黑樹的轉換?
- 鏈表轉爲紅黑樹
從直觀上來看肯定鏈表比較長了,纔會考慮轉爲紅黑樹,其實就是這麼回事,看看進行轉換的地方。
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 這裏就是轉爲紅黑樹的地方,當鏈表長度超過TREEIFY_THRESHOLD-1時就會進行轉換
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
- 紅黑樹轉爲鏈表
當刪除元素時,如果紅黑樹就沒幾個節點了,其實沒必要採用紅黑樹結構了,看看轉換的地方。
if (loHead != null) {
// 這裏就是當節點數小於一定的之後,纔會考慮從紅黑樹結果恢復爲鏈表結構
//不管怎麼樣,維持一個鏈表結構比紅黑樹結構簡單多了
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
擴展知識:
- 爲什麼capacity採用2的冪次方?
主要是減少插入元素時的衝突碰撞,下面看看求key的位置運算
tab[i = (n - 1) & hash]
使用按位與運算求得key的位置。所有key的hash值分佈的比較均勻,當n(即capacity)爲2的冪次方時,能保證key均勻分佈在每一個位置上。因爲2的冪次方-1後,二進制表示是每一位都是1,1&1=1,1&0=0比較均勻,如果n-1的二進制有一位有0的情況,不管hash對應的那一位是0還是1,最後的位與運算都是0,導致不均勻。
- 爲什麼hashMap是線程不安全?
沒有加鎖操作,容易導致鏈表產生環結構,查詢時陷入死循環。