1. 程式人生 > 實用技巧 >HashMap的put kv,是如何擴容的?

HashMap的put kv,是如何擴容的?

HashMap的put kv,是如何擴容的?

描述下HashMap put(k,v)的流程?
它的擴容流程是怎麼樣的?

HashMap put(k,v)流程

  1. 通過hash(key方法)獲取到key的hash值
  2. 呼叫put方法, 將value存放到指定的位置
    1. 根據hash值確定當前key所在node陣列的索引 (n - 1) & hash
    2. 如果node[i]==null 則直接建立新陣列
    3. 如果node[i]!=null
      1. 判斷 當前node的頭結點的 hash和key是否都相等, 相等則需要操作的就是該node
      2. 判斷當前節點是否為TreeNode,對TreeNode進行操作,並返回結果e
      3. 如果是連結串列則遍歷連結串列,key存在則返回節點e,不存在則賦值
      4. 判斷節點e有沒有被賦值,覆蓋舊值
    4. hashMap size進行加1,同時判斷v新size是否大於擴容閾值從而判斷是否需要擴容
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // 宣告Node陣列tab, Node節點
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 對tab陣列賦值為當前HashMap的table, 並判斷是否為空, 或者長度為0
        // 為0進行則resize()陣列, 並對 n賦值為當前tab的長度
        // resize() 對HashMap的table擴容, 並返回擴容後的新陣列
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 對 node p 進行賦值, 陣列所在位置 即 node p 如果是null 則直接賦值
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            // p 不為null, 宣告 node e, key k
            Node<K,V> e; K k;
            // 如果hash值相等且key相等, 直接將 e 賦值為當前node的頭節點
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
            // 如果是紅黑樹, 則對樹進行操作, 返回節點e
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 對連結串列進行遍歷, 找到對應的節點
                for (int binCount = 0; ; ++binCount) {
                    // 將 e 賦值為  頭節點p的next, 如果下一個節點為null
                    if ((e = p.next) == null) {
                        // 對節點進行賦值
                        p.next = newNode(hash, key, value, null);
                        // 如果長度到達數轉換閾值, 則需要轉換為紅黑樹
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果e節點的hash相等, key相等, 則 直接跳出迴圈 e 已經被賦值為 p.next
                    // 此時e節點的value沒有被賦值
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    // 指標指向下一個節點, 繼續遍歷
                    p = e;
                }
            }
        
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                // 對舊值進行覆蓋, 並返回舊值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 是否需要擴容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

resize()擴容過程

  1. JDK 1.7 擴容流程, 每次都需要陣列擴容後, 連結串列需要重新計算在新陣列的位置
  2. JDK 1.8 不需要重新計算 (優化點)
    1. 陣列下標: (n - 1) & hash 即陣列長度-1 & key的hash
    2. 擴容後的陣列下標: ((n << 1) - 1) & hash 相當於在 高位1之前加了個1

如圖所示, 真正發生影響的是新增的那一位(紅色箭頭所指), 所以 oldCap & hash 完全可以判斷該值是放在舊索引值的位置還是放在舊索引值+舊陣列長度的位置


final Node<K,V>[] resize() {
    // 舊陣列
    Node<K,V>[] oldTab = table;
    // 舊陣列長度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 舊的擴容閾值
    int oldThr = threshold;
    // 新的陣列長度和新擴容閾值
    int newCap, newThr = 0;
    // 舊陣列存在
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 新陣列長度為舊陣列長度的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 擴容閾值是舊擴容閾值的2倍
            newThr = oldThr << 1; // double threshold
    }
    // 舊陣列不存在, 相當於首次put(K, V)時, 將陣列長度置為擴容閾值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        // 舊陣列不存在, new HashMap()未指定長度, 初次put(K, V), 設定為預設值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 新的擴容閾值是0, 則將擴容閾值設定為 新陣列長度*負載因子
    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"})
    // 建立新陣列, 長度為新長度, 即原陣列長度的2倍
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    // 將table複製為新陣列
    table = newTab;
    if (oldTab != null) {
        // 對舊陣列進行遍歷
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            // 舊節點node賦值
            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 { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        // next節點
                        next = e.next;
                        // 節點hash與舊陣列長度 & 的結果來決定元素所在位置, 參考上面圖示所講
                        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) {
                        loTail.next = null;
                        // 索引j處直接賦值
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        // 索引 j + 老陣列長度位置存放hiHead
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}