如下操做相對正整數的二進制而言,對非整數不太適用。java
在二進制中,位權是2的冪,因此每一位所表明的權值從右到左分別爲2^(1-1) 、2^(2-1) 、... 、 2^(n-1) ,第n位的權值爲2的(n-1)次冪。
因此: 100101 = 2^5 + 2^2 + 2^0 = 37。數組
當一個二進制數左移一位,右補"0"的時候,這個數每一位的權值就變成了原來的兩倍,那麼整個數值也擴大了2倍;當這個數左移n位的時候,這個數就擴大到原來的2^n 倍。一樣的,往右移動n位,左補"0",至關於除以2^n ,若是考慮到右邊數位被捨棄的問題,這就至關於除以2^n 而後取整數。這和十進制是同樣的,十進制數字左移n位,就是擴大到原來的10^n 倍……app
在HashMap和ThreadLocal源碼中能夠看到相似這樣的操做:
在這裏,「按位與」操做的做用是取模,其中"n"和"len"都是數組的長度,此處代碼是要把元素的hash值映射成數組的索引(下標),以此來決定該元素的存儲位置。因爲hash值相對於數組的長度來講很大,因此不能把hash值直接一一映射爲數組下標,而是對其取模,經過餘數來映射。關於詳細的取模的意義,詳見百度-散列表。ide
當n等於2的次冪時,"m%n"和"m&(n-1)"等價,求證以下:
設n=16,hash=2740216402
3d
當n取2的冪時,n的二進制表示有個特色——除去左邊補全的0外,數字以"1"開頭,後面全是"0";n-1的二進制表示也有一個特色——n-1的二進制位數比n少一位,數位左邊全是"0",右邊全是"1"。code
n-1與hash值進行「按位與」操做時,就至關於把hash前面部分捨去,只保留後面部分(這與掩碼
把hash值2740216402的二進制表示拆成兩部分,可變爲:
解析: r爲保留部分,hash=p+r
p與n是存在倍數關係的,以下所示:
總結上述數量關係,可得:"hash = p + r = q * n + r",又因爲r比n小,因此r天然就是"hash % n"的餘數,所以當n等於2的冪,hash&(n-1)=hash%n。blog
在HashMap源碼中有這樣一段代碼:索引
/** * Computes key.hashCode() and spreads (XORs) higher bits of hash * to lower. Because the table uses power-of-two masking, sets of * hashes that vary only in bits above the current mask will * always collide. (Among known examples are sets of Float keys * holding consecutive whole numbers in small tables.) So we * apply a transform that spreads the impact of higher bits * downward. There is a tradeoff between speed, utility, and * quality of bit-spreading. Because many common sets of hashes * are already reasonably distributed (so don't benefit from * spreading), and because we use trees to handle large sets of * collisions in bins, we just XOR some shifted bits in the * cheapest possible way to reduce systematic lossage, as well as * to incorporate impact of the highest bits that would otherwise * never be used in index calculations because of table bounds. */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
先簡單瞭解下java的位移操做:源碼
關於異或的知識:
再來畫下圖看這裏代碼幹了些什麼:
好了,圖已經畫出來了,接下來對該操做進行解讀——代碼先是這樣……而後那樣……最後又……
解讀個鬼啊,其實這樣很難看出這段操做的意義,因此仍是看代碼上面的註釋吧,註釋寫得很清楚了,這段代碼主要做用是利用hashcode的結果來生成更加「散列」的哈希值hash,什麼意思?接着往下看:
接着上一小節的「按位與」講起,思考下,若是用原始的"hashcode"執行上小節的「按位與操做」會與怎樣的問題。
引用上面的舊圖分析,若是直接用原始的hashcode來取模,而後映射爲數組的下標,這樣會產生一個很大的問題。一般數組的長度不會太大,即上圖紅棕色的部分不會很長,那麼原始的hashcode的「高位」對最後的餘數的影響會很小,意思就是,只要hashcode後面的四位數爲"0010",無論前面藍紫色部分是什麼,「hashcode&(n-1)」的結果始終爲"0010",映射爲數組的下標就是「2」,這樣會很是容易形成「哈希衝突」(又名「哈希碰撞」)。
因此須要採起一種策略,使得hashcode的每一位,都儘可能參與運算,儘可能對取模結果產生影響,充分利用hashcode的每一位,使得取模的結果更加「零散」。所以,HashMap的源碼給出了以上的方法。
hashcode長度爲32位,右移16位,就是給原始的hashcode「折成兩半」,把高位的一半與低位的一半對齊,而後經過異或操做把高位和低位「結合」起來。
生成的新的hash值,其高位部分(左邊16位藍紫色部分)保留了原hashcode的高位,低位部分(紅色部分)保留了原來的高位和低位的「特徵」——若是原來高位部分某一位發生改變,則影響到結果的對應位;若是原來低位某一位發生改變,也一樣影響到結果相應的位。
這裏有一個問題,爲何要用異或操做?由於只能用異或操做,由於「與」和「或」不能很好的保留操做數的特徵:
當用上述方法生成新的hash值後,原來的hashcode的每一位都對最終的取模結果產生了影響,這時在必定程度上可使得生成的餘數更加均勻,更加「散列」,使得發生「碰撞」的概率下降。