【源】終於明白JDK8 HashMap底層數組長度,取值2次冪的原因

jdk1.8中的hashmap作了很多改進:紅黑樹的引入,鏈表尾插,以及底層數組長度保持2的次冪。本文專注於分析2次冪設定的原因,且聽我慢慢道來……

與“取餘”等價的算法

衆所周知,hashmap是數組鏈表結構:hash算法用於將key散列,經計算分散到數組槽中;而兩個key算出了同樣的值,即產生hash衝突時,就需要將槽中的單個節點升級成鏈表。由於get時需要對鏈表其進行遍歷,鏈表越長檢索效率越差。那麼,計算出的key值落點越平均,hash衝突的可能性越小

key值落點的計算方式爲,key的hash值數組長度取餘操作,記作key.hascode % array.length。從數學角度考慮,保持array.length爲質數會使得計算結果更均衡,hashTable就是這麼做的(數組初始值11)。但 hashmap 中 array.length 偏偏選擇了2的次冪,是個合數……何故?完全出於性能考慮!
先給出結論——當 array.ength長度是2的次冪時,key.hashcode % array.length等於key.hashcode & (array.length - 1)。下面重點看下這個結論是怎麼得出來的。

舉個例子:
假如 array.length = 2^4 = 16,二進制10000。這個數減去1的結果是1111,也就是array.length -1 = 1111。
(下面這段中的數字都是二進制)
再假設一個key的值爲10011011001(很隨意寫的一個數),與1111做 & 操作,得到的結果是1001(高位部分1001101都捨去了)。而1001必然是一個小於10000的數,對於一個小於10000的數而言,1001 % 10000得到的就是1001自己。
那麼剛剛捨棄的高位部分1001101 0000(後面補上了四個0000)就一定能被10000整除嗎?答案是肯定的:因爲10011010000可以拆成10000000000+10000000+1000000+10000,這幾個數都能通過10000的n次左移得到,也就相當於這幾個數都能被10000整除。那他們的和,也就是10011010000,一定也可以被10000整除。
因此,最終結論就是:10011011001 & ( 10000 - 1 ) = 10011011001 & 1111 = 1001 = 10011011001 % 10000

放張簡圖再嘮叨一遍以示總結,加深下印象:
clipboard.png

再強調一次:當 array.ength長度是2的次冪時,key.hashcode % array.length等於key.hashcode & (array.length - 1)

好,如果你讀懂了例子部分,相信你已經基本明白這個結論是站得住腳的(雖然不是純數學型的講解)。那麼hashmap的作者Doug Lea大神,爲什麼如此執着於用&操作替換%操作呢?
因爲對於二進制生物計算機來說,& 的效率要高於 %!(與、或、非都可看作二進制基本操作,同或、異或次之,+ - * ÷ % 等都基於前面的)

擴容時方便定位

這還不算完,好處不止這一處。
當hashmap需要擴容,重新計算鏈表元素的hashcode,以進行元素的重新定位時,依然能從“ 數組2次冪 ”的這個設定中借力!

hashmap數組擴容時,新數組length = 原數組length * 2,沿用前面的例子(array.length = 2^4 = 16,二進制10000),array.length 乘以 2 ,即二進制左移一位,由 10000 變成 100000。此時需要重新計算數組槽中的元素位置,如果槽中是鏈表,鏈表中每個元素都需要重新計算位置(這裏不考慮紅黑樹)。

計算的公式不變,key.hashcode & (array.length - 1),由於數組的翻倍(10000->100000),導致 array.length - 1 發生了改變(1111->11111)。此時,擴容前原本被捨棄的高位部分的最後1位,也將參與計算。
clipboard.png

在擴容這個歷史的拐點,這一位就顯得很特別:如果這個位置是0,餘數計算的將保持結果不變,意味着擴容後此元素還在這個槽中(槽編號沒發生改變);如果這個位置是1,餘數計算結果就變成了原槽索引 + 原array.length
也就是說,hashmap擴容的元素遷移過程中,由於數組大小是2次冪的巧妙設定,使得只要檢查 “ 特殊位 ” 就能確定該元素的最終定位。

給出一個較完整的擴容示意圖進行說明:
clipboard.png

  • 擴容前

紅綠黃三個元素,由各自的hashcode取餘後都淤積在數組槽13,組成以鏈表形式

  • 擴容後

紅、綠二星所表示的元素的hashcode“ 特殊位 ”爲0,取餘依然定位在槽13;而黃星表示的元素,hashcode“ 特殊位 ”爲1,取餘後結果 = 原槽索引 + 原數組大小 = 13 + 16 = 29。(這個結果也和圖中黃星的hashcode二進制低位值11101一致)

總結

對hashmap而言,數組長度始終保持2次冪有兩點好處:

  1. 能利用 & 操作代替 % 操作,提升性能
  2. 數組擴容時,只需關注 “特殊位” 就可以從新定位元素

性能,性能,還是性能……

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