equals() 與 hashCode() 是 Java 對象身份識別的雙子星,其契約規則直接決定 HashMap、HashSet、Hashtable 等哈希容器的正確性與性能。本文從契約定義、底層存取邏輯、七大實測場景、常見違約後果、最佳實踐封裝五個維度,徹底講透這對「孿生兄弟」。
x.equals(x) == truex.equals(y) == y.equals(x)x.equals(y) && y.equals(z) → x.equals(z)x.equals(null) == falsex.equals(y) == true,則 x.hashCode() == y.hashCode() 必須成立。x.equals(y) == false,hashCode() 可以相等(哈希碰撞),但應盡量分散以提高性能。hashCode() 必須返回相同值(除非修改用於 hashCode 計算的字段)。黃金法則:覆蓋 equals 必須覆蓋 hashCode,且兩者使用相同字段集合計算。
// HashMap.put 簡化邏輯
public V put(K key, V value) {
int hash = hash(key.hashCode()); // 1. 計算 hash
int index = (n - 1) & hash; // 2. 定位桶位
for (Node e = table[index]; e != null; e = e.next) {
// 3. 遍歷鏈表/紅黑樹,比較 hash 後再比較 equals
if (e.hash == hash && (e.key == key || key.equals(e.key))) {
V old = e.value;
e.value = value;
return old;
}
}
// 4. 未找到,新建節點插入
addNode(hash, key, value, index);
return null;
}
// HashSet.add 內部調用 HashMap.put
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
關鍵流程:
hashCode() → 擾動函數 → 桶索引。equals() 精確比較。測試類:Student,字段 age、name。集合:List(允許重複)、HashSet(去重)。
| 場景 | equals | hashCode | List.size | Set.size | 現象解析 |
|---|---|---|---|---|---|
| 1. 未覆蓋任何方法 | Object 默認(地址比較) | Object 默認(地址哈希) | 2 | 2 | 兩個 new 對象地址不同,equals=false,hash 不同,Set 視為不同元素 |
| 2. 僅覆蓋 equals(age+name) | 內容比較 → true | Object 默認(地址哈希) | 2 | 2 | 關鍵:equals=true 但 hash 不同!Set 先比 hash,以為不同桶,直接插入,違約導致去重失效 |
| 3. 僅覆蓋 hashCode(age+name) | Object 默認 → false | 內容計算 → 相同 | 2 | 2 | hash 相同進入同桶,但 equals=false,Set 視為不同元素(正常但效率低) |
| 4. 同時覆蓋 equals+hashCode(同字段) | 內容比較 → true | 內容計算 → 相同 | 2 | 1 | 正確:equals=true 且 hash 相同,Set 正確去重 |
| 5. equals=true 但 hashCode 故意返回不同值 | 內容比較 → true | 故意加干擾因子 → 不同 | 2 | 2 | 嚴重違約:equals 相等但 hash 不等,Set 視為不同元素,contains/remove 全失效 |
| 6. 放入 Set 後修改參與 hashCode 計算的字段 | 內容比較 | 依賴 age/name | 2 | 1 → remove 失敗仍為 1 | 災難:對象在錯誤桶位,remove() 找不到,**內存洩漏、業務錯誤** |
| 7. final 常量優化 | — | — | — | — | static final 編譯期常量折疊進常量池,讀取不觸發類初始化,hashCode 穩定 |
public final class Student {
private final int age;
private final String name; // final 字段,構造後不可變,天然安全
public Student(int age, String name) {
this.age = age;
this.name = name;
}
// IDE 自動生成(Alt+Insert),審查字段選擇
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(age, name); // JDK 7+ Objects.hash 自動處理 null、組合 hash
}
// getter、toString...
}
String、Integer、LocalDate、自定義 final 類。消除「入集合後修改字段」風險。Objects.deepEquals/Arrays.hashCode 並文檔化警告。getClass() 檢查會破壞多態性(建議用 instanceof 但需極度小心)。hashCode,避免重複計算(String 即採用此策略)。@Id 字段實現 equals/hashCode(業務鍵),而非對象地址。注意托管狀態下 ID 可能為 null(持久化前)。equals 定義「業務上的相等」,hashCode 定義「哈希桶分布」。同字段、同邏輯、同時覆蓋、不可變字段 是不敗公式。理解 HashMap 「先 hash 後 equals」的兩級過濾機制,能從根源上避開 90% 的哈希容器疑難雜癥。