1. 程式人生 > 實用技巧 >面試題:HashMap擴容機制

面試題:HashMap擴容機制

擴容機制

1.什麼時候才需要擴容
  • 在首次呼叫put方法的時候,初始化陣列table

  • 當HashMap中的元素個數超過陣列大小(陣列長度)*loadFactor(負載因子)時,就會進行陣列擴容,loadFactor的預設值(DEFAULT_LOAD_FACTOR)是0.75,這是一個折中的取值。也就是說,預設情況下,陣列大小為16,那麼當HashMap中的元素個數超過16×0.75=12(這個值就是閾值或者邊界值threshold值)的時候,就把陣列的大小擴充套件為2×16=32,即擴大一倍,然後重新計算每個元素在陣列中的位置,而這是一個非常耗效能的操作,所以如果我們已經預知HashMap中元素的個數,那麼預知元素的個數能夠有效的提高HashMap的效能。

  • 當HashMap中的其中一個連結串列的物件個數如果達到了8個,此時如果陣列長度沒有達到64,那麼HashMap會先擴容解決,如果已經達到了64,那麼這個連結串列會變成紅黑樹,節點型別由Node變成TreeNode型別。當然,如果對映關係被移除後,下次執行resize方法時判斷樹的節點個數低於6,也會再把樹轉換為連結串列。

2.HashMap的擴容是什麼

進行擴容,會伴隨著一次重新hash分配,並且會遍歷hash表中所有的元素,是非常耗時的。在編寫程式中,要儘量避免resize。

HashMap在進行擴容時,使用的rehash方式非常巧妙,因為每次擴容都是翻倍,與原來計算的 (n-1)&hash的結果相比,只是多了一個bit位,所以節點要麼就在原來的位置,要麼就被分配到"原位置+舊容量

"這個位置。

怎麼理解呢?例如我們從16擴充套件為32時,具體的變化如下所示:

因此元素在重新計算hash之後,因為n變為2倍,那麼n-1的標記範圍在高位多1bit(紅色),因此新的index就會發生這樣的變化:

說明:5是假設計算出來的原來的索引。這樣就驗證了上述所描述的:擴容之後所以節點要麼就在原來的位置,要麼就被分配到"原位置+舊容量"這個位置。

因此,我們在擴充HashMap的時候,不需要重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就可以了,是0的話索引沒變,是1的話索引變成“原索引+oldCap(原位置+舊容量)”。可以看看下圖為16擴充為32的resize示意圖:

正是因為這樣巧妙的rehash方式,既省去了重新計算hash值的時間,而且同時,由於新增的1bit是0還是1可以認為是隨機的,在resize的過程中保證了rehash之後每個桶上的節點數一定小於等於原來桶上的節點數,保證了rehash之後不會出現更嚴重的hash衝突,均勻的把之前的衝突的節點分散到新的桶中了。

3. 原始碼resize方法的解讀

下面是程式碼的具體實現:

final Node<K,V>[] resize() {
    //得到當前陣列
    Node<K,V>[] oldTab = table;
    //如果當前陣列等於null長度返回0,否則返回當前陣列的長度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //當前閥值點 預設是12(16*0.75)
    int oldThr = threshold;
    int newCap, newThr = 0;
    //如果老的陣列長度大於0
    //開始計算擴容後的大小
    if (oldCap > 0) {
        // 超過最大值就不再擴充了,就只好隨你碰撞去吧
        if (oldCap >= MAXIMUM_CAPACITY) {
            //修改閾值為int的最大值
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        /*
        	沒超過最大值,就擴充為原來的2倍
        	1)(newCap = oldCap << 1) < MAXIMUM_CAPACITY 擴大到2倍之後容量要小於最大容量
        	2)oldCap >= DEFAULT_INITIAL_CAPACITY 原陣列長度大於等於陣列初始化長度16
        */
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //閾值擴大一倍
            newThr = oldThr << 1; // double threshold
    }
    //老閾值點大於0 直接賦值
    else if (oldThr > 0) // 老閾值賦值給新的陣列長度
        newCap = oldThr;
    else {// 直接使用預設值
        newCap = DEFAULT_INITIAL_CAPACITY;//16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 計算新的resize最大上限
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //新的閥值 預設原來是12 乘以2之後變為24
    threshold = newThr;
    //建立新的雜湊表
    @SuppressWarnings({"rawtypes","unchecked"})
    //newCap是新的陣列長度--》32
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    //判斷舊陣列是否等於空
    if (oldTab != null) {
        // 把每個bucket都移動到新的buckets中
        //遍歷舊的雜湊表的每個桶,重新計算桶裡元素的新位置
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                //原來的資料賦值為null 便於GC回收
                oldTab[j] = null;
                //判斷陣列是否有下一個引用
                if (e.next == null)
                    //沒有下一個引用,說明不是連結串列,當前桶上只有一個鍵值對,直接插入
                    newTab[e.hash & (newCap - 1)] = e;
                //判斷是否是紅黑樹
                else if (e instanceof TreeNode)
                    //說明是紅黑樹來處理衝突的,則呼叫相關方法把樹分開
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // 採用連結串列處理衝突
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    //通過上述講解的原理來計算節點的新位置
                    do {
                        // 原索引
                        next = e.next;
                     	//這裡來判斷如果等於true e這個節點在resize之後不需要移動位置
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 原索引+oldCap
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 原索引放到bucket裡
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 原索引+oldCap放到bucket裡
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}