JDK1.7中,resize時,index取得時,全部采用重新hash的方式進行了。JDK1.8對這個進行了改善。
以前要確定index的時候用的是(e.hash & oldCap-1),是取模取余,而這里用到的是(e.hash & oldCap),它有兩種結果,一個是0,一個是oldCap,
比如oldCap=8,hash是3,11,19,27時,(e.hash & oldCap)的結果是0,8,0,8,這樣3,19組成新的鏈表,index為3;而11,27組成新的鏈表,新分配的index為3+8;
JDK1.7中重寫hash是(e.hash & newCap-1),也就是3,11,19,27對16取余,也是3,11,3,11,和上面的結果一樣,但是index為3的鏈表是19,3,index為3+8的鏈表是
27,11,也就是說1.7中經過resize后數據的順序變成了倒敘,而1.8沒有改變順序。
原理:
我們使用的是2次冪的擴展(指長度擴為原來2倍),所以,元素的位置要么是在原位置,要么是在原位置再移動2次冪的位置。看下圖可以明白這句話的意思,n為table的長度,圖(a)表示擴容前的key1和key2兩種key確定索引位置的示例,圖(b)表示擴容后key1和key2兩種key確定索引位置的示例,其中hash1是key1對應的哈希與高位運算結果。
元素在重新計算hash之后,因為n變為2倍,那么n-1的mask范圍在高位多1bit(紅色),因此新的index就會發生這樣的變化:
因此,我們在擴充HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”,可以看看下圖為16擴充為32的resize示意圖:
這個設計確實非常的巧妙,既省去了重新計算hash值的時間,而且同時,由於新增的1bit是0還是1可以認為是隨機的,因此resize的過程,均勻的把之前的沖突的節點分散到新的bucket了。這一塊就是JDK1.8新增的優化點。有一點注意區別,JDK1.7中rehash的時候,舊鏈表遷移新鏈表的時候,如果在新表的數組索引位置相同,則鏈表元素會倒置,但是從上圖可以看出,JDK1.8不會倒置。