「在覆蓋
equals時,必須覆蓋hashCode。」 —— 《Effective Java》第 3 版,條目 11
違反此法則會導致該類無法在 HashMap、HashSet 等基於哈希的集合中正常工作:邏輯上相等的對象可能被放入不同桶,導致 get 返回 null、contains 返回 false。
public final class PhoneNumber {
private final short areaCode, prefix, lineNumber;
public PhoneNumber(int areaCode, int prefix, int lineNumber) {
this.areaCode = (short) areaCode;
this.prefix = (short) prefix;
this.lineNumber = (short) lineNumber;
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof PhoneNumber)) return false;
PhoneNumber pn = (PhoneNumber) o;
return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode;
}
// 故意不覆蓋 hashCode → 繼承 Object.hashCode()(基於對象地址)
}
public static void main(String[] args) {
Map m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");
System.out.println(m.get(new PhoneNumber(707, 867, 5309))); // null!
@Override
public int hashCode() {
int result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
return result;
}
31 * i 可優化為 (i << 5) - i。hashCode()(處理 null 返回 0);數組用 Arrays.hashCode;boolean用 (f ? 1 : 0)。public final class PhoneNumber {
// ... 字段、構造器、equals 同前 ...
// 緩存哈希碼,volatile 保證發布安全性
private volatile int hashCode = 0; // 0 表示未計算
@Override
public int hashCode() {
int result = hashCode;
if (result == 0) { // 首次計算
result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
hashCode = result; // 寫入緩存
}
return result;
}
}
關鍵點:
volatile 確保多線程環境下,一個線程計算完成後,其他線程可見最新值(雙重檢查鎖模式的簡化版)。String、BigInteger 等 JDK 類,內部已採用此優化。equals() and hashCode()@EqualsAndHashCode(可指定 onlyExplicitlyIncluded = true 手動控制字段)equals/hashCode/toString。return 42;)equals 與 hashCode 是一對「雙生契約」。覆蓋其一必覆蓋另一,且**依賴字段集合完全一致、全部不可變**。對於高頻使用的不可變值類,添加 volatile 緩存哈希碼是極低成本的性能優化。下次重寫 equals 時,請同時按 Alt+Insert 生成 hashCode,並檢查字段不可變性——這是專業 Java 開發者的基本功。