JDK 1.8 中 HashMap 擴(kuò)容

問:簡單說說 JDK 1.8 中 HashMap 是如何擴(kuò)容的?與 JDK 1.7 有什么區(qū)別?

答:JDK 1.7 中 HashMap 的擴(kuò)容機(jī)制簡單總結(jié)如下圖:

可以看見,1.7 中整個擴(kuò)容過程就是一個取出數(shù)組元素(實際數(shù)組索引位置上的每個元素是每個獨立單向鏈表的頭部,也就是發(fā)生 Hash 沖突后最后放入的沖突元素)然后遍歷以該元素為頭的單向鏈表元素,依據(jù)每個被遍歷元素的 hash 值計算其在新數(shù)組中的下標(biāo)然后進(jìn)行交換(即原來 hash 沖突的單向鏈表尾部變成了擴(kuò)容后單向鏈表的頭部)。

而在 JDK 1.8 中 HashMap 的擴(kuò)容操作就顯得更加的騷氣了,由于擴(kuò)容數(shù)組的長度是 2 倍關(guān)系,所以對于假設(shè)初始 tableSize = 4 要擴(kuò)容到 8 來說就是 0100 到 1000 的變化(左移一位就是 2 倍),在擴(kuò)容中只用判斷原來的 hash 值與左移動的一位(newtable 的值)按位與操作是 0 或 1 就行,0 的話索引就不變,1 的話索引變成原索引加上擴(kuò)容前數(shù)組,所以其實現(xiàn)如下流程圖所示:

上圖就是 1.8 與 1.7 擴(kuò)容的核心流程圖區(qū)別,其 1.8 源碼核心實現(xiàn)如下:

        final Node<K, V>[] resize () {
            Node<K, V>[] oldTab = table;
            //記住擴(kuò)容前的數(shù)組長度和最大容量
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            int oldThr = threshold;
            int newCap, newThr = 0;
            if (oldCap > 0) {
                //超過數(shù)組在java中最大容量就無能為力了,沖突就只能沖突
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                //長度和最大容量都擴(kuò)容為原來的二倍 
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                        oldCap >= DEFAULT_INITIAL_CAPACITY)
                    newThr = oldThr << 1;
                double threshold
            }
            //...... ......
            //更新新的最大容量為擴(kuò)容計算后的最大容量
            threshold = newThr;
            //更新擴(kuò)容后的新數(shù)組長度
            Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
            table = newTab;
            if (oldTab != null) {
                //遍歷老數(shù)組下標(biāo)索引
                for (int j = 0; j < oldCap; ++j) {
                    Node<K, V> e;
                    //如果老數(shù)組對應(yīng)索引上有元素則取出鏈表頭元素放在e中
                    if ((e = oldTab[j]) != null) {
                        oldTab[j] = null;
                        //如果老數(shù)組j下標(biāo)處只有一個元素則直接計算新數(shù)組中位置放置
                        if (e.next == null)
                            newTab[e.hash & (newCap - 1)] = e;
                        else if (e instanceof TreeNode)
                            //如果是樹結(jié)構(gòu)進(jìn)行單獨處理
                            ((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                        else {
                            preserve order
                            //能進(jìn)來說明數(shù)組索引j位置上存在哈希沖突的鏈表結(jié)構(gòu)
                            Node<K, V> loHead = null, loTail = null;
                            Node<K, V> hiHead = null, hiTail = null;
                            Node<K, V> next;
                            //循環(huán)處理數(shù)組索引j位置上哈希沖突的鏈表中每個元素
                            do {
                                next = e.next;
                                //判斷key的hash值與老數(shù)組長度與操作后結(jié)果決定元素是放在原索引處還是新索引
                                if ((e.hash & oldCap) == 0) {
                                    //放在原索引處的建立新鏈表
                                    if (loTail == null) loHead = e;
                                    else loTail.next = e;
                                    loTail = e;
                                } else {
                                    //放在新索引(原索引 + oldCap)處的建立新鏈表
                                    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;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            return newTab;
        }

可以看見,這個設(shè)計非常贊,因為 hash 值本來就是隨機(jī)性的,所以 hash 按位與上 newTable 得到的 0(擴(kuò)容前的索引位置)和 1(擴(kuò)容前索引位置加上擴(kuò)容前數(shù)組長度的數(shù)值索引處)就是隨機(jī)的,所以擴(kuò)容的過程就能把之前哈希沖突的元素再隨機(jī)的分布到不同的索引去,這算是 JDK1.8 的一個優(yōu)化點。

此外,在 JDK1.7 中擴(kuò)容操作時,哈希沖突的數(shù)組索引處的舊鏈表元素擴(kuò)容到新數(shù)組時,如果擴(kuò)容后索引位置在新數(shù)組的索引位置與原數(shù)組中索引位置相同,則鏈表元素會發(fā)生倒置(即如上面圖1,原來鏈表頭擴(kuò)容后變?yōu)槲舶停?;而?JDK1.8 中不會出現(xiàn)鏈表倒置現(xiàn)象。

其次,由于 JDK1.7 中發(fā)生哈希沖突時僅僅采用了鏈表結(jié)構(gòu)存儲沖突元素,所以擴(kuò)容時僅僅是重新計算其存儲位置而已,而 JDK1.8 中為了性能在同一索引處發(fā)生哈希沖突到一定程度時鏈表結(jié)構(gòu)會轉(zhuǎn)換為紅黑數(shù)結(jié)構(gòu)存儲沖突元素,故在擴(kuò)容時如果當(dāng)前索引中元素結(jié)構(gòu)是紅黑樹且元素個數(shù)小于鏈表還原閾值(哈希沖突程度常量)時就會把樹形結(jié)構(gòu)縮小或直接還原為鏈表結(jié)構(gòu)(其實現(xiàn)就是上面代碼片段中的 split() 方法)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容