1. 程式人生 > >9. 【對於所有物件都通用的方法】重寫equals方法時一定也要重寫hashCode方法

9. 【對於所有物件都通用的方法】重寫equals方法時一定也要重寫hashCode方法

本文是《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方法呢:

  1. 選擇一個非零的常數值,比如19;
  2. 對於物件中的每個關鍵域f(就是equals方法中涉及的每個域),完成如下步驟:
    1. 為該域計算int型別的雜湊碼c:
      1. 如果該域是boolean型別,則計算(f ? 1 : 0)
      2. 如果該域是bytecharshortint型別,則計算(int)f
      3. 如果該域是long型別,則計算(int)(f ^ (f >>> 32))
      4. 如果該域是float型別,則計算Float.floatToIntBits(f)
      5. 如果該域是double型別,則計算Double.doubleToLongBits(f),然後在按照2.1.3計算;
      6. 如果該域是一個物件引用,如果值為null,則返回0;否則返回該物件的hashCode值;
      7. 如果該域是一個數組,則可以利用Arrays.hashCode方法來處理。
    2. 按照下邊的公式計算result result = 31 * result + c;
  3. 返回result;
  4. 編寫單元測試來驗證。

其中,應該注意,如果一個域的值可以根據參與計算的其他域的值計算出來,那麼這樣的域應該排除在外。此外,之所以經常選擇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;
    }