Java equals() 與 hashCode()
equals() 和 hashCode() 是 Object 類別的兩個重要方法,正確實作它們對於集合操作至關重要。
預設行為
Object obj1 = new Object();
Object obj2 = new Object();
// equals 預設比較參考(記憶體位址)
obj1.equals(obj2); // false
obj1.equals(obj1); // true
// hashCode 預設由記憶體位址衍生
System.out.println(obj1.hashCode()); // 例如:366712642
equals() 契約
正確的 equals() 實作必須滿足:
- 自反性:
x.equals(x)必須為true - 對稱性:
x.equals(y)為true,則y.equals(x)也為true - 傳遞性:
x.equals(y)和y.equals(z)為true,則x.equals(z)為true - 一致性:多次呼叫
x.equals(y)結果一致 - 非空性:
x.equals(null)必須為false
覆寫 equals()
public class Person {
private String name;
private int age;
@Override
public boolean equals(Object obj) {
// 1. 自反性:同一物件
if (this == obj) return true;
// 2. 非空性
if (obj == null) return false;
// 3. 類型檢查
if (getClass() != obj.getClass()) return false;
// 4. 欄位比較
Person person = (Person) obj;
return age == person.age &&
Objects.equals(name, person.name);
}
}
使用 instanceof(需謹慎)
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Person)) return false; // 允許子類別
Person person = (Person) obj;
return age == person.age &&
Objects.equals(name, person.name);
}
hashCode() 契約
- 相等的物件必須有相同的 hashCode
- hashCode 相同的物件不一定相等
- 在同一次執行中,hashCode 必須一致
覆寫 hashCode()
public class Person {
private String name;
private int age;
@Override
public int hashCode() {
return Objects.hash(name, age);
}
// 或手動計算
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + age;
return result;
}
}
完整範例
public class Person {
private String name;
private int age;
private String email;
public Person(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age &&
Objects.equals(name, person.name) &&
Objects.equals(email, person.email);
}
@Override
public int hashCode() {
return Objects.hash(name, age, email);
}
}
為何必須同時覆寫
// 只覆寫 equals,不覆寫 hashCode
public class BadPerson {
private String name;
@Override
public boolean equals(Object obj) {
if (obj instanceof BadPerson) {
return name.equals(((BadPerson) obj).name);
}
return false;
}
// 沒有覆寫 hashCode!
}
BadPerson p1 = new BadPerson("Alice");
BadPerson p2 = new BadPerson("Alice");
p1.equals(p2); // true
Set<BadPerson> set = new HashSet<>();
set.add(p1);
set.contains(p2); // false!因為 hashCode 不同
使用 IDE 或 Record
IDE 自動生成
大多數 IDE 都可以自動生成 equals() 和 hashCode()。
使用 Record(Java 16+)
public record Person(String name, int age, String email) {}
// 自動實作 equals 和 hashCode
Person p1 = new Person("Alice", 25, "alice@example.com");
Person p2 = new Person("Alice", 25, "alice@example.com");
p1.equals(p2); // true
p1.hashCode(); // 自動生成
注意事項
可變物件
// 危險:將可變物件放入 HashSet 後修改
Set<Person> set = new HashSet<>();
Person p = new Person("Alice", 25);
set.add(p);
p.setAge(30); // 修改後 hashCode 改變!
set.contains(p); // 可能為 false
繼承問題
// 使用 getClass() 而非 instanceof 可避免子類別問題
if (getClass() != obj.getClass()) return false;
重點整理
- 覆寫
equals()必須同時覆寫hashCode() - 相等的物件必須有相同的 hashCode
- 使用
Objects.equals()處理 null 安全 - 使用
Objects.hash()簡化 hashCode 計算 - 避免在集合中修改影響 hashCode 的欄位
- Record 類型自動實作這兩個方法