HashMap中resize()剖析

HashMap中resize()剖析java

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 只有非第一次擴容纔會進來(第一次擴容在第一次put)
    if (oldCap > 0) {
        // oldCap最大爲MAXIMUM_CAPACITY(2^30),可查看帶參構造方法①
        if (oldCap >= MAXIMUM_CAPACITY) {
             /**
                 * threshold變成MAX_VALUE(2^31-1),隨它們碰撞。可是oldCap不改變,
                 * 由於若是oldCap翻倍就爲負數了,若是賦值爲MAX_VALUE,
                 * 參考 Map容量爲何不能爲MAX_VALUE②
                 */
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 容量翻倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            /**
             * 爲何須要判斷oldCap >= DEFAULT_INITIAL_CAPACITY呢?
             * 應該是容量較小時 capacity * loadFactor形成的偏差比較大,
             * 例如初始化容量爲2 threshold則爲1,若是每次擴容threshold都翻倍,
             * 那負載因子是0.5了。
             * 爲何只小於16呢?
             * 我猜想是在每次擴容都計算threshold和用位運算翻倍之間作權衡
             */
            newThr = oldThr << 1; 
    }
    // 帶參初始化會進入這裏,主要是爲了從新算threshold
    else if (oldThr > 0) 
        newCap = oldThr;
    // 不帶參初始化會進入這裏
    else {               
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 從新算threshold
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    // 擴容
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 複製數據到新table中
    if (oldTab != null) {
        // 遍歷Node
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                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 { 
                    // 之因此定義兩個頭兩個尾對象,是因爲鏈表中的元素的下標在擴容後,要麼是原下標+oldCap,要麼不變,下面會證明
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    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) {
                        // 尾部節點next設置爲null,代碼嚴謹
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 新下標對應的鏈表
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
 
①帶參構造方法
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    // 容量最大爲MAXIMUM_CAPACITY(2^30)
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    this.loadFactor = loadFactor;
    // threshold初始化爲最接近initialCapacity的2的冪次方,而且大於或等於initialCapacity。可是在第一次put的時候,threshold會變成threshold * loadFactor
    this.threshold = tableSizeFor(initialCapacity);
}
 
②Map容量爲何不能爲MAX_VALUE
該爲題可轉爲:爲何在Java1.8,每次擴容都爲2的冪次方呢?
// 計算下標,下面是map的put和get中都用到計算下標的
(n - 1) & hash
 
當容量爲MAX_VALUE(2^31-1)時,轉換成二進制
    hash
&
    0111 1111 1111 1111 1111 1111 1111 1110
-----------------------------------------------
        xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx0
從上面可看出最低位不管hash是任何值時,都爲0,也就是下標只有2^30種可能,有2^30-1個下標沒有被使用
因此當容量爲MAX_VALUE(2^31-1)時會形成一半的空間浪費,效率等同於MAXIMUM_CAPACITY(2^30)
 
③e.hash & oldCap
該步驟是爲了計算位置是否須要移動
由於oldTab的元素下標是根據 hash(key) & (oldCap-1) 計算的,若是擴容後,計算下標是 hash(key) & (2*oldCap-1)
換成二進制就比較清晰了

其中看出低位和高位的亦或主要是是hash分佈均勻。數組

treeifyBin方法,應該能夠解釋爲:把容器裏的元素變成樹結構。當HashMap的內部元素數組中某個位置上存在多個hash值相同的鍵值對,這些Node已經造成了一個鏈表,當該鏈表的長度大於等於9this

/**
 * tab:元素數組,
 * hash:hash值(要增長的鍵值對的key的hash值)
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
 
    int n, index; Node<K,V> e;
    /*
     * 若是元素數組爲空 或者 數組長度小於 樹結構化的最小限制
     * MIN_TREEIFY_CAPACITY 默認值64,對於這個值能夠理解爲:若是元素數組長度小於這個值,沒有必要去進行結構轉換
     * 當一個數組位置上集中了多個鍵值對,那是由於這些key的hash值和數組長度取模以後結果相同。(並非由於這些key的hash值相同)
     * 由於hash值相同的機率不高,因此能夠經過擴容的方式,來使得最終這些key的hash值在和新的數組長度取模以後,拆分到多個數組位置上。
     */
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize(); // 擴容,可參見resize方法解析
 
    // 若是元素數組長度已經大於等於了 MIN_TREEIFY_CAPACITY,那麼就有必要進行結構轉換了
    // 根據hash值和數組長度進行取模運算後,獲得鏈表的首節點
    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); // 繼續遍歷鏈表
 
        // 到目前爲止 也只是把Node對象轉換成了TreeNode對象,把單向鏈表轉換成了雙向鏈表
 
        // 把轉換後的雙向鏈表,替換原來位置上的單向鏈表
        if ((tab[index] = hd) != null)
            hd.treeify(tab);//此處單獨解析
    }
}

後續部分繼續補充。
參考博客:https://blog.csdn.net/weixin_42340670/article/details/80503863
https://blog.csdn.net/u010828343/article/details/80769385.net

相關文章
相關標籤/搜索