重寫jdk源碼:HashMap的resize方法優化思考

        友情提示:本文推理過程是不準確的,因爲在HashMap中處於數組同一位置的元素的哈希值大部分情況是不同的,但整個思考過程比較完整,有興趣的可以看看。


話不多說,我們直接看HashMap的resize方法源碼:

重點在715-744行,我直接說結論,我會用一行代碼去替換掉這近30行,如下:

newTab[e.hash & (newCap - 1)] = e;

你可能看出來了,這和上面的 e.next == null 判斷之後的操作是一樣的,所以可以直接將這2個判斷合併重寫之後的resize方法如下:

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;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        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"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                       newTab[e.hash & (newCap - 1)] = e;
                    }
                }
            }
        }
        return newTab;
    }

  

分析思考:

下面來分析一下我的思考過程,其實我首先想到的是下面這個版本:

if ((e.hash & oldCap) == 0) {
	newTab[j] = e;
}else {
	newTab[j+ oldCap]= e;
}

然後覺得能不能更優,就有了下面這個2行的代碼:

  newTab[e.hash & oldCap == 0?j:j+ oldCap] = e;

本來都這兒可以結束了,但我突然發現,還可以更簡潔,所以有了開頭的那一行代碼,和源碼中的712行一樣,所以直接就能合併了。

在我發現了這個優化後,是考慮弄個類繼承HashMap去測試驗證一下,但由於resize方法不是public修飾的,且是final關鍵字修飾的,所以必須得將整個 HashMap的源碼複製過來,再修改其resize方法即可實現驗證,我覺得太麻煩,而且也沒必要。

所以這裏呢我會以原作者的思路,通過逆推的方式來驗證我這個優化的準確性。我們繼續來分析一下resize方法的 715-744行 代碼,它首先定義了 loHead,loTail 和 hiHead,hiTail 這四個變量,目的就是爲了區分 (e.hash & oldCap) == 0 和  (e.hash & oldCap) != 0 的情況,這個情況是因爲在HashMap擴容後 e.hash & 2n-1 的值只可能是原數組的索引下標 j 或者原數組的索引下標加上原數組的長度  j + oldCap 。但其實完全沒必要定義四個變量,只需要定義2個 head 和 tail 即可。爲什麼呢?現在我們着重看下do-while 循環中的代碼:

重點在 721 行,可以看出來 e.hash() 和 oldCap  這2個變量在 do-while 循環中 是不變的,因爲整個鏈表e都是在原數組的同一個位置的,所以它們的哈希值肯定是相等的。基於這個結論,我們在原作者的思路上可以一下優化他的715-744行代碼,如下:

                        Node<K,V> head = null, tail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if (tail == null)
                                head = e;
                            else
                                tail.next = e;
                            tail = e;
                        } while ((e = next) != null);
                        if (tail != null) {
                            tail.next = null;
                            newTab[(head.hash & oldCap) == 0?j:j+ oldCap] =  head;
                        }

只用了 head 和 tail 2個變量就完成了原來的功能。寫到這兒,我想你大概明白了,分析上面的代碼可以看出來,其實 head 和 tail 變量並沒有什麼實際用處。在do-while 循環結束後先判斷 tail 是否爲空 ,這行完全沒必要,因爲在588行已經判斷了e不爲null了,如下:

所以進入循環後 tail 肯定不會爲空的,然後用 head 的哈希值與 oldCap 按位與運算後得到在數組中的索引位置,將head放入新數組中,這行根本就是多餘的,因爲 head=e 只會在第一次循環的時候賦值,所以整個do-while 循環沒有必要,直接用 e 替換head是完全沒問題的,這樣用 e 替換後就得到了我開頭優化的那行代碼:

newTab[(e.hash & oldCap) == 0?j:j+ oldCap] = e;

然後發現 e.hash 值既然是固定的,就沒必要用三元表達式了,直接用 e.hash & (newCap - 1) 得到下標索引就行了:

 newTab[e.hash & (newCap - 1)] = e;

所以我們用原作者的思路推出了我們這個優化代碼。

總結:

我第一次看到 715-744行 這段代碼是有點不知所云的,總覺得有些彆扭,在看了一遍之後,我得到的結論就是newTab[j] = e;

所以在經過思考和分析後就得到了這一行優化代碼:

 newTab[e.hash & (newCap - 1)] = e;

另外在源碼中可以感覺到有點花裏胡哨,弄得很複雜。事實上我們回過頭來思考,會發現 715-744行整個的目的就是將原數組中的鏈表順序複製進新數組中即可,避免jdk1.7中擴容數據遷移時候的死循環,所以其實很簡單就能做到了

如果覺得有問題的同學,可以在評論中指出來

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章