Java 1.8 HashMap擴容原理

1. HashMap源碼講解

源碼講解網上太多了,能夠參考HashMap此生來世。 我這裏主要講解Java 8的HashMap擴容原理。下文中聲明的桶與數組的含義是一致的html

1. 先介紹hash()方法,之因此不用Object自帶的hashCode方法是怕別人僞造相同hash值的key,使得哈希衝突。該方法註釋上說了一大堆,意思大概是爲了減小哈希衝突,讓高16位參與運算。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製代碼

2. 再來介紹一下HahsMap是如何定位到桶的位置?

i = (tab.length - 1) & hash
複製代碼

這與咱們平時學的hash表算法不太同樣,先說效果:位運算計算快,效率高,與數組長度取模(好比Hashtabal的put方法就是哈希取模)效果同樣。 由於桶的個數是2的n次冪,以默認16舉例。tab.length -1 =15,二進制就是 1111,與hash進行&運算,獲得的結果是hash值的最後4位。運算規則參考我上一篇文章算法

3. 終於來到了擴容部分,咱們以桶個數默認16,擴容到32爲例子,直接看源碼。

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        // oldCap = 16
        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;
            }
            // newCap =oldCap << 1 = 32
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        ···// 省略其餘代碼,下面就是擴容代碼,數據怎麼轉移到新數組newTab中
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            // oldCap是舊桶的個數,也就是沒擴容以前數組的長度,這裏等於16 。這裏遍歷桶
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null; //清除桶元素,利於回收
                    // 若是桶中元素後面沒有其餘元素,直接計算新index,插入新桶中
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //  若是桶中元素還有下一個元素,節點類型是TreeNode類型,那麼按照TreeNode方式插入。這個比較複雜    
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    // 到了這個分支,就說明節點類型是鏈表,咱們主要分析這個
                    else { 
                    // preserve order 
                    // 3.1 
                        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) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            // j + oldCap是由於高位,正好是2的4次方,也就是oldCap的長度
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

複製代碼

3.1

這裏註釋preserve order說明了下面鏈表的遷移保持順序,讓我想起來Java 1.7的HashMap在擴容的時候,全部元素所有從新計算Hash&(NewTable.length-1),致使擴容後的鏈表沒有保持順序,因此在併發的Rehash狀況下會致使死循環shell

來看看核心判斷條件(e.hash & oldCap) == 0。這裏oldCap=16,就是Hash值與10000進行或運算(只有兩個數都爲1,結果才爲1)。計算結果也就是判斷Hash值的第5個bit位是否等於0。這裏爲何要判斷第5個bit位呢?由於以前計算桶下標的時候是直接取的hash值的前4位,若是擴容後,計算t桶下標方式就是(hash & (newCap - 1)) ,newCap =32,也就是hash & (100000-1),也就是與11111進行或運算,獲得的結果就是hash值的前5位。看到這裏不知道你是否有點迷糊,那就看個圖。數組

因此判斷條件(e.hash & oldCap) == 0 計算出來的值,若是等於0,就放入低位節點,低位節點在新數組的位置跟原數組同樣。 等於1,就放入高位節點,高位放在新數組的位置等於原來位置在加上原數組的長度,由於這個1是計算出來的第5個bit位,因此是10000,也就是2的4次方,16。正好等於原來數組長度。

看到這裏,你會發現這個設計很是的巧妙,避免了所有從新Rehash的過程,而且新增的bit爲能夠認爲是隨機的,這樣子就能夠將以前衝突的數據分散到新的桶中。bash

參考部分:併發

HashMap今世前身ide

Jdk1.8中的HashMap實現原理post

相關文章
相關標籤/搜索