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() 實作必須滿足:

  1. 自反性x.equals(x) 必須為 true
  2. 對稱性x.equals(y)true,則 y.equals(x) 也為 true
  3. 傳遞性x.equals(y)y.equals(z)true,則 x.equals(z)true
  4. 一致性:多次呼叫 x.equals(y) 結果一致
  5. 非空性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() 契約

  1. 相等的物件必須有相同的 hashCode
  2. hashCode 相同的物件不一定相等
  3. 在同一次執行中,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 類型自動實作這兩個方法