equals() 是 Java 對象比較的核心方法,但許多開發者僅停留在「比較內容」的表層理解。本文從內存模型、源碼實現、重寫規範、常見陷阱四個維度深度剖析,助你構建完整知識體系。
public class Cat {
int color, weight, height;
public Cat(int color, int weight, int height) {
this.color = color;
this.weight = weight;
this.height = height;
}
}
public static void main(String[] args) {
Cat c1 = new Cat(1, 1, 1);
Cat c2 = new Cat(1, 1, 1);
System.out.println("c1 == c2: " + (c1 == c2)); // false - 比較引用地址
System.out.println("c1.equals(c2): " + c1.equals(c2)); // false - 默認也比較引用
}
內存圖解:
c1、c2 存儲堆地址(如 0x1000、0x2000)Cat 對象,屬性值相同但地址不同== 比較 0x1000 == 0x2000 → falseequals 同樣比較地址 → false
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false - 兩個對象
System.out.println(s1.equals(s2)); // true - String 重寫了 equals
String.equals 源碼邏輯:
value[] 數組內容根據 java.lang.Object 規範,重寫必須滿足:
| 契約 | 數學定義 | 代碼含義 |
|---|---|---|
| 自反性 | x.equals(x) == true | 對象必須等於自己 |
| 對稱性 | x.equals(y) == y.equals(x) | 雙向一致 |
| 傳遞性 | x.equals(y) && y.equals(z) → x.equals(z) | 鏈式相等 |
| 一致性 | 多次調用結果不變 | 無副作用影響 |
| 非空性 | x.equals(null) == false | 不拋異常,直接 false |
public class Person {
private String firstName;
private String lastName;
private int age;
// Getter/Setter 省略...
@Override
public boolean equals(Object obj) {
// 1. 引用相同快速返回
if (this == obj) return true;
// 2. 非空檢查
if (obj == null) return false;
// 3. 類型檢查 - 使用 getClass() 保證嚴格類型匹配
if (getClass() != obj.getClass()) return false;
// 4. 轉型並比較核心字段
Person other = (Person) obj;
return age == other.age
&& Objects.equals(firstName, other.firstName)
&& Objects.equals(lastName, other.lastName);
}
// 重寫 equals 必須重寫 hashCode!
@Override
public int hashCode() {
return Objects.hash(firstName, lastName, age);
}
}
class Animal {
String species;
// equals 只比較 species
}
class Dog extends Animal {
String name;
// 如果 Dog.equals 比較 name + species
// 會破壞對稱性:animal.equals(dog) ≠ dog.equals(animal)
}
解決方案:使用 getClass() 而非 instanceof,或使類 final、或文檔說明不支持跨類比較。
Set set = new HashSet<>();
set.add(new Person("John", "Doe", 30));
set.add(new Person("John", "Doe", 30)); // 依賴 equals + hashCode 去重
System.out.println(set.size()); // 正確輸出 1
若只重寫 equals 不重寫 hashCode,HashSet/HashMap 會失效(視為不同對象)。
IDE 生成的代碼常使用 instanceof,可能導致對稱性違反。建議:
getClass()Objects.equals 處理 null 安全
@EqualsAndHashCode(of = {"id"}) // 僅按 ID 比較
@Entity
public class User {
@Id Long id;
String name;
String email;
}
// 自動生成基於所有字段的 equals/hashCode/toString
public record Point(int x, int y) {}
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
System.out.println(p1.equals(p2)); // true
使用 EqualsVerifier 庫自動驗證契約:
@Test
public void equalsContract() {
EqualsVerifier.forClass(Person.class).verify();
}
| 場景 | 推薦做法 |
|---|---|
| 值對象 | 比較所有業務字段,用 getClass(),配合 Record 或 Lombok |
| 實體類 | 僅比較 ID(業務主鍵) |
| DTO/VO | 比較所有字段,通常不可變 |
| 集合元素/Map Key | 必須重寫 equals + hashCode 保持一致 |
| 枚舉 | 無需重寫,天生單例支持 == 比較 |
掌握 equals 的核心在於:理解引用與值的區別、遵守五大契約、配合 hashCode、根據對象角色選擇比較策略。這是寫出健壯 Java 代碼的必修課。