HashMap中的位運算

二進制基礎回顧

如下操做相對正整數的二進制而言,對非整數不太適用。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前面部分捨去,只保留後面部分(這與掩碼 相似,實際上在源碼註釋部分,也把這操做稱爲"mask")。這實際上就是取模操做,後面的保留部分棕紅色的0010就是「餘數」。爲何這部分是餘數?接着往下求證: orm

  把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中的異或操做

在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的位移操做:源碼

  • <<:左移運算符,num << 1,至關於num乘以2
  • >>:右移運算符,num >> 1,至關於num除以2
  • >>>:無符號右移,忽略符號位,空位都以0補齊

關於異或的知識:

  • A ^ 0 = A,即當0與一個數(0/1)進行異或操做時,結果等於這個數自己,如0 ^ 0 = 0、 0 ^ 1 = 1;
  • A ^ 1 = ! A,即當1與一個數(0/1)進行異或操做,結果等於非-此數,或者說取反,如1 ^ 0 = 1, 1 ^ 1 = 0;

再來畫下圖看這裏代碼幹了些什麼:

  好了,圖已經畫出來了,接下來對該操做進行解讀——代碼先是這樣……而後那樣……最後又……
  解讀個鬼啊,其實這樣很難看出這段操做的意義,因此仍是看代碼上面的註釋吧,註釋寫得很清楚了,這段代碼主要做用是利用hashcode的結果來生成更加「散列」的哈希值hash,什麼意思?接着往下看:
  接着上一小節的「按位與」講起,思考下,若是用原始的"hashcode"執行上小節的「按位與操做」會與怎樣的問題。

  引用上面的舊圖分析,若是直接用原始的hashcode來取模,而後映射爲數組的下標,這樣會產生一個很大的問題。一般數組的長度不會太大,即上圖紅棕色的部分不會很長,那麼原始的hashcode的「高位」對最後的餘數的影響會很小,意思就是,只要hashcode後面的四位數爲"0010",無論前面藍紫色部分是什麼,「hashcode&(n-1)」的結果始終爲"0010",映射爲數組的下標就是「2」,這樣會很是容易形成「哈希衝突」(又名「哈希碰撞」)。
  因此須要採起一種策略,使得hashcode的每一位,都儘可能參與運算,儘可能對取模結果產生影響,充分利用hashcode的每一位,使得取模的結果更加「零散」。所以,HashMap的源碼給出了以上的方法。
  hashcode長度爲32位,右移16位,就是給原始的hashcode「折成兩半」,把高位的一半與低位的一半對齊,而後經過異或操做把高位和低位「結合」起來。

  生成的新的hash值,其高位部分(左邊16位藍紫色部分)保留了原hashcode的高位,低位部分(紅色部分)保留了原來的高位和低位的「特徵」——若是原來高位部分某一位發生改變,則影響到結果的對應位;若是原來低位某一位發生改變,也一樣影響到結果相應的位。
  這裏有一個問題,爲何要用異或操做?由於只能用異或操做,由於「與」和「或」不能很好的保留操做數的特徵:

  • 使用「與」操做時,當一個數爲「0」,則結果必然爲「0」,沒必要考慮另外一個操做數;
  • 使用「或」操做時,當一個操做數爲「1」,則結果必然爲「1」;
  • 使用「異或」操做時,須要知道兩個操做數才能決定結果。

  當用上述方法生成新的hash值後,原來的hashcode的每一位都對最終的取模結果產生了影響,這時在必定程度上可使得生成的餘數更加均勻,更加「散列」,使得發生「碰撞」的概率下降。

相關文章
相關標籤/搜索