JDK 1.8 HashMap擴容原理

擴容原理

  • 首先明確一下擴容以後所有node節點本質還是通過(n-1)&hash 得到索引,然後放入對應的node數組槽位中,但是jdk的開發者在這裏使用了等價的計算方式加速了rehash的過程,將所有的node節點分成了需要移動和不需要移動兩個鏈表,然後一次性移動到對應的位置上
  • 要明白這個等價的計算方式是什麼,需要一個直觀的對比就可以很容易的理解,假設map當前容量是n=16(n-1 對應的二進制是0000 1111) ,node的hash值的二進制是1010 0101,那麼在擴容前node 的索引的計算是通過如下方式得到
擴容前計算索引
1010 0101
& 0000 1111
0000 0101
索引結果 5

擴容以後容量是n=32(對應的二進制是0001 1111),node本身的hash值是不變的,仍然是1010 0101,那麼擴容後node 的索引的計算是通過如下方式得到

擴容後計算索引
1010 0101
& 0001 1111
0000 0101
索引結果 5

發現計算後的索引結果和擴容前是一樣的,那麼是什麼原因導致擴容前後的索引是一樣的?或者是不一樣的呢?我們把擴容前後的兩次hash計算索引的過程放在一起對比一下

擴容前計算索引 擴容後計算索引
(hash) 1010 0101 101 0 0101
(n-1)& 0000 1111 & 000 1 1111
0000 0101 0000 0101
索引結果 5 5

先給出結論,注意看一下(n-1) 的值他轉化爲二進制以後的第五位,也就是表中加粗的數字

  • 它的左邊高位都是0,做&運算以後無論hash值中對應的位是0或1,最後結果都是0,也就是不影響最後的計算結果
  • 它的右邊高位都是1,做&運算以後無論hash值中對應的位是0或1,最後結果都和原來的值保持一致,這是&的計算特性
  • 結合上兩條特點,那麼無論擴容前後,低四位&以後的計算結果都是一樣的都是5(0101),唯一影響計算結果的就是第5位,也就是說node的hash值的二進制如果第5位是0,那麼擴容後索引不變,如果是1,擴容後的索引就變了,那麼會變成什麼呢?舉個具體例子看一下
  • 假設現在node的hash值是1011 0101,相比之前的node,現在例子中的hash值二進制第五位是1
擴容前計算索引 擴容後計算索引
(hash) 1011 0101 101 1 0101
&(n-1) 0000 1111 & 000 1 1111
0001 0101 000 1 0101
索引結果 5 16 + 5

我們發現,最後擴容以後計算結果中第5位變成了1,低四位不變,也就是16 + 5,而這增加的16恰好是map擴容前的容量16,回到上面的問題,node的hash值的二進制如果第5位是1,擴容後的索引就是 擴容前的容量 + 原索引值。

  • 那麼爲什麼是第五位呢?因爲擴容一倍以後,n-1的二進制位從4個1(1111)變爲了5個1(11111),影響計算結果的恰恰是多出來的那一位1,那麼現在可以再總結一下上面的規律,如何判斷一個node在擴容後索引是否變化?如果多出來的那一位1和hash做位與運算結果中第五位是0,那麼擴容後索引不變,如果是1那麼擴容後的索引就是 擴容前的容量 + 原索引值
  • 根據上面的結論,計算結果就可以簡化爲如下過程
hash值二進制第五位是1 hash值二進制第五位是0
1011 0101 101 0 0101
& 0001 0000 & 000 1 0000
0001 0000 000 0 0000
第五位結果 1 0
  • 根據表格中計算過程可知,根據第五位的結果就能確定擴容後的索引
  • 走到這裏,就引出了最後一個問題,影響結果的第五位如何確定呢?我們發現進行計算的二進制數0001 0000 = 16 ,正好就是擴容前的容量值,所以最後的結論就是:
  • hash & oldcap = 0 擴容後索引不變
  • hash & oldcap !=0 擴容後 索引= oldcap + 原索引值,這樣回頭再去看java中hashmap 擴容的原理源碼就一目瞭然了
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;//首次初始化後table爲Null
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;//默認構造器的情況下爲0
        int newCap, newThr = 0;
        if (oldCap > 0) {//table擴容過
             //當前table容量大於最大值得時候返回當前table
             if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
            //table的容量乘以2,threshold的值也乘以2           
            newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
        //使用帶有初始容量的構造器時,table容量爲初始化得到的threshold
        newCap = oldThr;
        else {  //默認構造器下進行擴容  
             // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
        //使用帶有初始容量的構造器在此處進行擴容
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                HashMap.Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    // help gc
                    oldTab[j] = null;
                    if (e.next == null)
                        // 當前index沒有發生hash衝突,直接對2取模,即移位運算hash &(2^n -1)
                        // 擴容都是按照2的冪次方擴容,因此newCap = 2^n
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof HashMap.TreeNode)
                        // 當前index對應的節點爲紅黑樹,這裏篇幅比較長且需要了解其數據結構跟算法,因此不進行詳解,當樹的個數小於等於UNTREEIFY_THRESHOLD則轉成鏈表
                        ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        // 把當前index對應的鏈表分成兩個鏈表,減少擴容的遷移量
                        HashMap.Node<K,V> loHead = null, loTail = null;
                        HashMap.Node<K,V> hiHead = null, hiTail = null;
                        HashMap.Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                // 擴容後不需要移動的鏈表
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                // 擴容後需要移動的鏈表
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            // help gc
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            // help gc
                            hiTail.next = null;
                            // 擴容長度爲當前index位置+舊的容量
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }


參考
https://www.jianshu.com/p/321fdf485970
https://blog.csdn.net/u010890358/article/details/80496144#擴容機制核心方法Node%3CK%2CV%3E%5B%5D%20resize()

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章