9. 【對於所有物件都通用的方法】重寫equals方法時一定也要重寫hashCode方法
阿新 • • 發佈:2019-02-04
本文是《Effective Java》讀書筆記第9條,其中內容可能會結合實際應用情況或參考其他資料進行補充或調整。
在每個覆蓋了equals
方法的類中,一定也要覆蓋hasCode
方法。否則會導致該類無法結合所有基於雜湊的集合(比如HashMap、HashSet、HashTable等)一起正常工作。
這一原則出自Java Object的規範(其實是第二條):
1. 在應用程式執行期間,只要物件的equals方法的比較操作所用到的資訊沒有被修改,那麼對這一物件呼叫多次,hashCode
方法必須始終返回統一整數。而在同一個應用程式的多次執行過程中,每次執行所返回的整數可以不一致。
2. 如果兩個物件的equals
hashCode
方法應該返回同樣的整數結果。而如果equals
方法比較結果是不相等的,那麼兩個物件的hashCode
方法不一定要返回不同的整數結果(應該注意的一點是,不相等的物件產生不同的整數結果,一定程度上可以提高散列表的效能)。 給一個簡單的例子:
public class PhoneNumber {
private final int areaCode;
private final int prefix;
private final int lineNumber;
public PhoneNumber(int areaCode, int prefix, int lineNumber) {
rangeCheck(areaCode, 999, "area code");
rangeCheck(prefix, 999, "prefix code");
rangeCheck(lineNumber, 9999, "line number");
this.areaCode = areaCode;
this.prefix = prefix;
this.lineNumber = lineNumber;
}
private static void rangeCheck(int arg, int max, String name) {
if (arg < 0 || arg > max)
throw new IllegalArgumentException(name + ":" + arg);
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
else if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.areaCode == this.areaCode && pn.prefix == this.prefix && pn.lineNumber == this.lineNumber;
}
// 這裡沒有覆蓋hasCode方法
public static void main(String[] args) {
HashMap<PhoneNumber, String> map = new HashMap<>();
PhoneNumber pn1 = new PhoneNumber(123, 456, 7890);
PhoneNumber pn2 = new PhoneNumber(123, 456, 7890);
System.out.println(pn1.equals(pn2));
map.put(pn1, "jack");
System.out.println(map.get(pn2));
}
}
可以看到,pn1和pn2是equal的。本來期望map.get
返回結果是“jack”,結果返回的是null
。這是由於PhoneNumber
沒有覆蓋hashCode
方法,導致兩個物件具有不同的雜湊碼,因而對於HashMap物件來說,這是兩個不同的key。
修正這一問題的方法就是提供一個適當的hashCode
方法。何為“適當”的hashCode
方法呢:
- 選擇一個非零的常數值,比如19;
- 對於物件中的每個關鍵域f(就是
equals
方法中涉及的每個域),完成如下步驟:
- 為該域計算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)
,然後在按照2.1.3計算; - 如果該域是一個物件引用,如果值為
null
,則返回0;否則返回該物件的hashCode
值; - 如果該域是一個數組,則可以利用
Arrays.hashCode
方法來處理。
- 如果該域是boolean型別,則計算
- 按照下邊的公式計算result
result = 31 * result + c;
- 為該域計算int型別的雜湊碼c:
- 返回result;
- 編寫單元測試來驗證。
其中,應該注意,如果一個域的值可以根據參與計算的其他域的值計算出來,那麼這樣的域應該排除在外。此外,之所以經常選擇31來乘以result,是因為31是一個奇素數,它有個很好的特性就是可以利用移位和減法來代替乘法運算,從而得到更好的效能,比如 31*i=(i<<<5)-i
。
最終,剛才的例子中的hashCode可以這麼寫:
@Override
public int hashCode() {
int result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
return result;
}
當然,如果計算雜湊碼的開銷特別大,那麼可以將雜湊碼快取在物件內部,而不是每次請求都計算雜湊碼:
private volatile int hasCode;
@Override
public int hashCode() {
int result = hasCode;
if (result == 0) {
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
hashCode = result;
}
return result;
}