在前面講解 HashMap 的源碼實現時,有以下幾點:html
①、初始容量爲 1<<4,也就是24 = 16算法
②、負載因子是0.75,當存入HashMap的元素佔比超過整個容量的75%時,進行擴容,並且在不超過int類型的範圍時,進行2次冪的擴展(指長度擴爲原來2倍)函數
擴大一倍spa
③、新添加一個元素時,計算這個元素在HashMap中的位置,也就是本篇文章的主角 哈希運算。分爲三步:code
第一步:取 hashCode 值: key.hashCode()htm
第二步:高位參與運算:h>>>16blog
第三步:取模運算:(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 行代碼是我本身加的。get
咱們知道一個好的 哈希算法可以使得元素分佈的更加均勻,從而減小哈希衝突。HashMap 在這塊的處理就很巧妙:源碼
首先第一步取得 hashCode,該方法是一個用native修飾的本地方法,返回的是一個 int 類型的值(根據內存地址換算出來的一個值),一般咱們都會重寫該方法。
第二步將取得的哈希值無符號右移16位,高位補0。並與前面第一步得到的hash碼進行按位異或^ 運算。這樣作有什麼用呢?這其實也是擾動函數,爲了下降哈希碼的衝突。右位移16位,正好是32bit的一半,高半區和低半區作異或,就是爲了混合原始哈希碼的高位和低位,以此來加大低位的隨機性。並且混合後的低位摻雜了高位的部分特徵,這樣高位的信息也被變相保留下來。也就是保證考慮到高低Bit位都參與到Hash的計算中。
有興趣的能夠看看JDK1.7中,實際上是作了4次擾動,在JDK1.8中只作了一次,我猜想是爲了在下降衝突的同時保證效率。
本文的重點是第三步,將通過前面兩步獲取的 hash 值,與HashMap的集合長度減 1 進行按位與 & 運算:(n-1) & hash。可是其實不少哈希算法,爲了使元素分佈均勻,都是用的取模運算,用一個值去模上總長度,即 n%hash。咱們知道在計算機中 & 的效率比 % 高不少,那麼如何將 % 轉換爲 & 運算呢?在HashMap 中,是用的 (n - 1) & hash 進行運算的,那麼這是爲何呢?
這就是本篇博客咱們將要明白的問題。
咱們先給出結論:
當 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。顯然是不成立的。
爲何是這樣?下面咱們來詳細分析。
首先咱們要知道以下規則:
①、"<<" 左移:右邊空出的位上補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次方,才知足上面的公式,不然就是錯誤的。
經過上面的分析過程了,咱們完美了證實了公式的正確性。在回到 HashMap 的實現過程,咱們知道HashMap的初始容量爲啥是 1<<4 了吧,並且每次擴容都是擴大一倍。由於必需要完美的知足 hash 算法。