友情提示:本文推理過程是不準確的,因爲在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中擴容數據遷移時候的死循環,所以其實很簡單就能做到了
如果覺得有問題的同學,可以在評論中指出來