1、引出問題
在前面講解
HashMap的源碼實作時,有如下幾點:
①、初始容量為 1<<4,也就是24 = 16

②、負載因子是0.75,當存入HashMap的元素占比超過整個容量的75%時,進行擴容,而且在不超過int類型的範圍時,進行2次幂的擴充(指長度擴為原來2倍)
擴大一倍
③、新添加一個元素時,計算這個元素在HashMap中的位置,也就是本篇文章的主角 哈希運算。分為三步:
第一步:取 hashCode 值: key.hashCode()
第二步:高位參與運算:h>>>16
第三步:取模運算:(n-1) & hash
1 static final int hash(Object key) {
2 int h;
3 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
4 }
5
6 tab[i = (n - 1) & hash];
ps:第 6 行代碼是我自己加的。
我們知道一個好的 雜湊演算法能夠使得元素分布的更加均勻,進而減少哈希沖突。HashMap 在這塊的處理就很巧妙:
首先第一步取得 hashCode,該方法是一個用native修飾的本地方法,傳回的是一個 int 類型的值(根據記憶體位址換算出來的一個值),通常我們都會重寫該方法。
第二步将取得的哈希值無符号右移16位,高位補0。并與前面第一步獲得的hash碼進行按位異或^ 運算。這是為了當length比較小的時候,也能保證考慮到高低Bit位都參與到Hash的計算中,同時不會有太大的開銷。
本文的重點是第三步,将經過前面兩步擷取的 hash 值,與HashMap的集合長度減 1 進行按位與 & 運算:(n-1) & hash。但是其實很多雜湊演算法,為了使元素分布均勻,都是用的取模運算,用一個值去模上總長度,即 n%hash。我們知道在計算機中 & 的效率比 % 高很多,那麼如何将 % 轉換為 & 運算呢?在HashMap 中,是用的 (n - 1) & hash 進行運算的,那麼這是為什麼呢?
這就是本篇部落格我們将要明白的問題。
2、結論
我們先給出結論:
當 lenth = 2n 時,X % length = X & (length - 1)
也就是說,長度為2的n次幂時,模運算 % 可以變換為按位與 & 運算。
比如:9 % 4 = 1,9的二進制是 1001 ,4-1 = 3,3的二進制是 0011。 9 & 3 = 1001 & 0011 = 0001 = 1
再比如:12 % 8 = 4,12的二進制是 1100,8-1 = 7,7的二進制是 0111。12 & 7 = 1100 & 0111 = 0100 = 4
上面兩個例子4和8都是2的n次幂,結論是成立的,那麼當長度不為2的n次幂呢?
比如:9 % 5 = 4,9的二進制是 1001,5-1 = 4,4的二進制是0100。9 & 4 = 1001 & 0100 = 0000 = 0。顯然是不成立的。
為什麼是這樣?下面我們來詳細分析。
3、分析過程
首先我們要知道如下規則:
①、"<<" 左移:右邊空出的位上補0,左邊的位将從字頭擠掉,左移一位其值相當于乘2。
②、">>"右移:右邊的位被擠掉,右移一位其值相當于除以2。對于左邊移出的空位,如果是正數則空位補0,若為負數,可能補0或補1,這取決于所用的計算機系統。
③、">>>"無符号右移,右邊的位被擠掉,對于左邊移出的空位一概補上0。
根據二進制數的特點,相信大家很好了解。
對于給定一個任意的十進制數XnXn-1Xn-2....X1X0,我們将其用二進制的表示方法分解:
XnXn-1Xn-2....X1X0 = Xn*2n+Xn-1*2n-1+......+X1*21+X0*20 3-1公式
這裡的十進制數隻有三位,同理當有N位時,後面2的幂次方依次從 0 開始遞增到 N 。
回到上面的結論: lenth = 2n 時,X % length = X & (length - 1)
以及對于除法,被除數是滿足配置設定率的(除數不滿足):
成立:(a+b)÷c=a÷c+b÷c 3-2公式
不成立:a÷(b+c)≠a÷c+b÷c
通過 3-1公式以及 3-2 公式,我們可以得出當任意一個十進制除以一個2k的數時,我們可以将這個十進制轉換成3-1公式的表示形式:
(XnXn-1Xn-2....X1X0) / 2k = (Xn*2n+Xn-1*2n-1+......+X1*21+X0*20) / 2k = Xn*2n / 2k +Xn-1*2n-1 / 2k +......+ X1*21 / 2k + X0*20 / 2k
如果我們想求上面公式的餘數,相信大家一眼就能看出來:
①、當 0<= k <= n 時,餘數為 Xk*2k+Xk-1*2k-1+......+X1*21+X0*20 ,也就是說 比 k 大的 n次幂,我們都舍掉了(大的都能整除 2k),比k小的我們都留下來了(小的不能整除2k)。那麼留來下來即為餘數。
②、當 k > n 時,餘數即為整個十進制數。
看到這裡,我們離證明結論已經很近了。再回到上面說的二進制的移位操作,向右移 n 位,表示除以 2n 次方,由此我們得到一個很重要的結論:
一個十進制數對一個2n 的數取餘,我們可以将這個十進制轉換為二進制數,将這個二進制數右移n位,移掉的這 n 位數即是餘數。
知道怎麼算餘數了,那麼我們怎麼去擷取這移掉的 n 為數呢?
我們再看20,21,22....2n 用二進制表示如下:
0001,0010,0100,1000,10000......
我們把上面的數字減一:
0000,0001,0011,0111,01111......
根據與運算符&的規律,當位上都是 1 時,結果才是 1,否則為 0。是以任意一個二進制數對 2k 取餘時,我們可以将這個二進制數與(2k-1)進行按位與運算,保留的即使餘數。
這就完美的證明了前面給出的結論:
當 lenth = 2n 時,X % length = X & (length - 1)
注意,一定要是2n次方,才滿足上面的公式,否則就是錯誤的。
4、總結
通過上面的分析過程了,我們完美了證明了公式的正确性。在回到 HashMap 的實作過程,我們知道HashMap的初始容量為啥是 1<<4 了吧,而且每次擴容都是擴大一倍。因為必須要完美的滿足 hash 算法。
作者:
YSOcean出處:
http://www.cnblogs.com/ysocean/本文版權歸作者所有,歡迎轉載,但未經作者同意不能轉載,否則保留追究法律責任的權利。