Java 基礎:解析 hashCode

Java 中全部的類都繼承自 Object 類,Object 類中有個返回 hashCode 的本地方法。java

public native int hashCode();
複製代碼

在文檔的註釋中很清楚的說明了 hashCode 的做用,和它應該知足的一些要求。算法

做用:給一個對象返回一個 hashCode 值,這個值在 hash table 的數據結構中有重要的做用。例如,肯定放置在 hash table 哪一個索引位置,hash 衝突的頻率。segmentfault

要求數組

  1. 同一個 Java 對象,在程序運行的整個生命週期中。該對象返回的 hashCode 應該相同。
  2. 使用 equals 方法,判斷爲兩個相等的對象,其返回的 hashCode 應該相同。
  3. 使用 equals 方法,判斷爲兩個不相同的對象,其返回的 hashCode 應該不相同。

一般的 hashCode 生成方法是將對象的內存地址轉換成一個整型數,這樣就能爲不一樣的對象返回一個不同的 hashCode。可是這種方法不能知足上面的第二個條件,因此這種實現也不是 Java 語言所必須的實現方法。數據結構

在 String 中的實現

String 類也是繼承自 Object 類,它重寫了 hashCode() 方法。優化

/** Cache the hash code for the string */
private int hash; // Default to 0

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}
複製代碼

在 String 中計算的 hashCode 會存儲在 hash 變量中,而且只會計算一次。由於 String 是 final 的,而且一個 String 對象被初始化後沒法修改,因此它的 hashCode 不會變化。spa

for 循環計算 hashCode 的方法是根據如下式子:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]。設計

使用 31 的緣由

31 是一個質數(Prime number),質數也稱爲素數。質數是大於 1 的天然數,且只能被 1 和其自己整除。code

選擇 31 的緣由大概有如下幾點:對象

  • 一個數乘質數後的結果,只能被 1 、質數、乘數還有結果自己整除。計算 hashCode 選擇一個優質質數能夠下降 hash 的衝突率。

  • 31 (2 << 5 - 1),在計算的過程當中能夠被 JVM 優化。

相信第二點不少同窗都可以理解,如今解釋一下第一點。

咱們列舉一下 100 之內左右的質數:2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97。

從上面的質數中選擇三個小中大質數:2,31,97。分析公式 s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] 中的每一項,都是一個數乘質數的平方項。因此咱們計算一下每一個質數的 n 次方,咱們選擇 n = 5。那麼結果以下:

質數 結果
2 2^5 = 32
31 31^5 = 28,629,151
97 97^5 = 8,587,340,257

能夠看到經過質數 2 計算後的 hashCode 值是一個比較小的值,而經過質數 97 計算後的 hashCode 是一個比較大的值,而 31 比較適中。

咱們能夠認爲 hashCode 的範圍若過小,可能會增長 hash 衝突的機率。而計算 hashCode 的計算乘子太大容易致使整型數的溢出(這裏並非說選擇 31 不會致使溢出,是指一個致使溢出的速率),從而也會致使 hash 衝突的機率。31 能夠有效的減輕這兩點。

更詳細的內容能夠看一下 stackoverflow 上面的這個問題:Why does Java's hashCode() in String use 31 as a multiplier?

設計 hashCode 算法

根據《Effective Java》第二版中的第 9 條,對於咱們本身編寫的類,覆蓋 equals 方法時須要覆蓋 hashCode 方法。緣由在前面說過。

那麼如何設計一個 hashCode 算法,書中設計了一個算法:

  1. 把某個非 0 的常數值,好比 17,保存在一個名爲 result 的 int 類型的變量中。
  2. 對於對象中的每一個域,作以下操做:
    • 爲該域計算 int 類型的哈希值 c :
      • 若是該域是 boolean 類型,則計算 (f?1:0)
      • 若是該域是 byte、char、short 或者 int 類型,則計算 (int)f
      • 若是該域是 long 類型,則計算 (int)(f^(f>>>32))
      • 若是該域是 float 類型,則計算 Float.floatToIntBits(f)
      • 若是該域是 double 類型,則計算 Double.doubleToLongBits(f),而後重複第三個步驟。
      • 若是該域是一個對象引用,而且該類的 equals 方法經過遞歸調用 equals 方法來比較這個域,一樣爲這個域遞歸的調用 hashCode,若是這個域爲 null,則返回0。
      • 若是該域是數組,則要把每個元素看成單獨的域來處理,遞歸的運用上述規則,若是數組域中的每一個元素都很重要,那麼可使用 Arrays.hashCode 方法。
    • 按照公式 result = 31 * result + c,把上面步驟 2.1 中計算獲得的散列碼 c 合併到 result 中。
  3. 返回 result

參考

科普:爲何 String hashCode 方法選擇數字31做爲乘子

Why does Java's hashCode() in String use 31 as a multiplier?

《Effective Java》

相關文章
相關標籤/搜索