在 Java 中,當你需要基于對象的內容而非引用地址來判斷兩個對象是否相等時,就需要重寫equals
和hashCode
方法。以下是具體場景和實現原則:
一、為什么需要同時重寫這兩個方法?
equals
方法:默認比較對象的內存地址(==
),若需比較內容(如兩個Person
對象的name
和age
是否相同),則需重寫。hashCode
方法:- HashMap/HashSet 依賴:這些集合通過
hashCode
快速定位元素,若不重寫,即使內容相同的對象也會被存儲多次(因為默認hashCode
基于內存地址計算)。 - 約定:Java 規范要求若兩個對象
equals
為true
,則hashCode
必須相同
- HashMap/HashSet 依賴:這些集合通過
hashCode
方法的重寫原則
- 相同對象必須返回相同哈希值(根據
equals
的結果)。 - 哈希值分布均勻:減少哈希沖突。
?
常見誤區
- 只重寫
equals
不重寫hashCode
:- 導致 HashMap/HashSet 無法正常工作(如無法正確去重)。
- 使用錯誤的哈希計算方式:
- 例如返回固定值(如
return 1;
),會導致所有對象哈希沖突,性能嚴重下降。
- 例如返回固定值(如
- 忽略父類屬性:
- 若父類有重要字段,需在
equals
和hashCode
中包含父類的判斷邏輯(通過super.equals()
和super.hashCode()
)。
- 若父類有重要字段,需在
?
哈希集合的工作原理
HashMap 的 put () 流程:
- 計算哈希值:通過
key.hashCode()
確定存儲桶的位置。 - 處理哈希沖突:若多個鍵的
hashCode
相同,在桶內通過equals()
比較鍵是否相等。- 若
equals()
為true
,則覆蓋原值; - 若
equals()
為false
,則以鏈表 / 紅黑樹形式存儲。
- 若
在 Java 中,正確重寫equals
和hashCode
方法需要遵循特定的規則和步驟。以下是詳細的實現指南和示例:
一、重寫equals
方法的步驟
1. 檢查對象引用是否相同
java
運行
if (this == o) return true;
2. 檢查對象是否為null
或類型不匹配
java
運行
if (o == null || getClass() != o.getClass()) return false;
3. 強制類型轉換并比較關鍵字段
使用Objects.equals
比較引用類型(處理null
安全),直接比較基本類型:
java
運行
Person person = (Person) o;
return age == person.age && // 基本類型用==Objects.equals(name, person.name); // 引用類型用Objects.equals
二、重寫hashCode
方法的步驟
1. 使用關鍵字段生成哈希值
推薦使用Objects.hash()
方法,它會自動處理null
并組合多個字段:
java
運行
@Override
public int hashCode() {return Objects.hash(name, age);
}
三、完整示例
以下是一個Person
類的完整實現:
java
運行
import java.util.Objects;public class Person {private String name;private int age;private String email; // 可選字段,可能為null// 構造方法、getter/setter略@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Person person = (Person) o;return age == person.age &&Objects.equals(name, person.name) &&Objects.equals(email, person.email);}@Overridepublic int hashCode() {return Objects.hash(name, age, email);}
}
四、進階技巧
1. 處理繼承關系
若父類也有重要字段,需調用父類的equals
和hashCode
:
java
運行
@Override
public boolean equals(Object o) {if (!super.equals(o)) return false; // 先檢查父類字段// 再比較子類字段...
}@Override
public int hashCode() {return Objects.hash(super.hashCode(), childField);
}
2. 處理數組字段
使用Arrays.equals
和Arrays.hashCode
:
java
運行
private int[] scores;@Override
public boolean equals(Object o) {// ...return Arrays.equals(scores, person.scores);
}@Override
public int hashCode() {return Objects.hash(name, Arrays.hashCode(scores));
}
3. 性能優化
若計算哈希值開銷大,可緩存結果(適用于不可變對象):
java
運行
private int hashCode; // 緩存哈希值@Override
public int hashCode() {int result = hashCode;if (result == 0) {result = Objects.hash(name, age);hashCode = result;}return result;
}
五、常見錯誤
錯誤示例 | 問題 |
---|---|
僅比較部分字段 | 如忽略email 字段,可能導致equals 邏輯不一致。 |
使用== 比較引用類型 | 如name == person.name ,未處理null 或不同實例但內容相同的情況(如new String("abc") )。 |
返回固定哈希值 | 如return 1; ,導致所有對象哈希沖突,哈希集合性能嚴重下降。 |
違反對稱性 | 如a.equals(b) 為true ,但b.equals(a) 為false (例如比較時忽略了子類字段)。 |
六、IDE 自動生成(以 IntelliJ IDEA 為例)
- 右鍵點擊類內空白處 →?
Generate
?→?equals() and hashCode()
- 選擇需要參與比較的字段
- 確認生成的代碼(IDE 通常會生成正確實現)
七、驗證重寫是否正確
編寫單元測試驗證行為:
java
運行
import static org.junit.Assert.*;public class PersonTest {@Testpublic void testEqualsAndHashCode() {Person p1 = new Person("Alice", 20, "alice@example.com");Person p2 = new Person("Alice", 20, "alice@example.com");assertTrue(p1.equals(p2)); // 內容相同,應返回trueassertEquals(p1.hashCode(), p2.hashCode()); // 哈希值必須相同Person p3 = new Person("Bob", 30, null);assertFalse(p1.equals(p3)); // 內容不同,應返回false}
}
總結
關鍵點 | 實現方法 |
---|---|
equals | 1. 引用相等檢查 2. 類型和 null 檢查3. 字段比較(使用 Objects.equals ) |
hashCode | 使用Objects.hash() 組合所有參與equals 比較的字段 |
繼承關系 | 調用父類的equals 和hashCode |
數組字段 | 使用Arrays.equals 和Arrays.hashCode |