談談HashMap結構那些事~(1.8版本)

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()
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是線程不安全?
    沒有加鎖操作,容易導致鏈表產生環結構,查詢時陷入死循環。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章