Java 中所有的類都繼承自 Object 類,Object 類中有個返回 hashCode 的本地方法。
public native int hashCode();
複製程式碼
在文件的註釋中很清楚的說明了 hashCode 的作用,和它應該滿足的一些要求。
作用:給一個物件返回一個 hashCode 值,這個值在 hash table 的資料結構中有重要的作用。例如,確定放置在 hash table 哪個索引位置,hash 衝突的頻率。
要求:
- 同一個 Java 物件,在程式執行的整個生命週期中。該物件返回的 hashCode 應該相同。
- 使用 equals 方法,判斷為兩個相等的物件,其返回的 hashCode 應該相同。
- 使用 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 不會變化。
for 迴圈計算 hashCode 的方法是根據以下式子:s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]。
使用 31 的原因
31 是一個質數(Prime number),質數也稱為素數。質數是大於 1 的自然數,且只能被 1 和其本身整除。
選擇 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 演算法,書中設計了一個演算法:
- 把某個非 0 的常數值,比如 17,儲存在一個名為 result 的 int 型別的變數中。
- 對於物件中的每個域,做如下操作:
- 為該域計算 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 中。
- 為該域計算 int 型別的雜湊值 c :
- 返回 result
參考
科普:為什麼 String hashCode 方法選擇數字31作為乘子
Why does Java`s hashCode() in String use 31 as a multiplier?
《Effective Java》