equals() 是 Java 對象邏輯相等性的核心判斷依據,直接影響 HashMap、HashSet、ArrayList.contains() 等集合操作的正確性。本文系統梳理 equals 五大契約、三類經典違反案例、標準實現模板及 IDE 自動生成技巧,助你寫出健壯、符合規範的相等性判斷邏輯。
| 契約 | 數學定義 | 代碼含義 | 違反後果 |
|---|---|---|---|
| 自反性 | x = x | x.equals(x) == true | 對象無法在 Set 中正常去重、作為 Map Key 查找失敗 |
| 對稱性 | x = y ⇒ y = x | x.equals(y) == y.equals(x) | 集合操作行為不可預測,set.contains(x) 與 set.contains(y) 結果不一致 |
| 傳遞性 | x = y ∧ y = z ⇒ x = z | x.equals(y) && y.equals(z) ⇒ x.equals(z) | 集合邏輯崩潰,如 ArrayList.indexOf 返回錯誤索引 |
| 一致性 | 不變則等 | 多次調用結果不變(對象未修改) | 可變對象作為 Key 導致 Map 查找失效 |
| 非空性 | x ≠ null | x.equals(null) == false | 拋 NPE 或邏輯錯誤 |
public class AntiReflexive {
private String str;
@Override
public boolean equals(Object o) {
if (!(o instanceof AntiReflexive)) return false;
AntiReflexive other = (AntiReflexive) o;
return this.str.equals(other.str.toLowerCase()); // ❌ 破壞自反性
}
public static void main(String[] args) {
AntiReflexive a = new AntiReflexive();
a.setStr("Hello");
System.out.println(a.equals(a)); // false!因為 "Hello".equals("hello") = false
}
}
修正:雙向統一轉換或不轉換 return this.str.equalsIgnoreCase(other.str);
public class CaseInsensitiveString {
private String s;
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(((CaseInsensitiveString)o).s);
if (o instanceof String) // ❌ 單向支持 String,但 String.equals 不知情
return s.equalsIgnoreCase((String)o);
return false;
}
}
// 測試
CaseInsensitiveString cis = new CaseInsensitiveString("abc");
String str = "abc";
System.out.println(cis.equals(str)); // true
System.out.println(str.equals(cis)); // false!String 不知道 CaseInsensitiveString 存在
鐵律:絕不在 equals 中支持與非本類型的比較,除非該類型是你控制的最終類(final class)。
class Point {
int x, y;
@Override public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
Point p = (Point) o;
return x == p.x && y == p.y;
}
}
class ColorPoint extends Point {
Color color;
@Override public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
if (!(o instanceof ColorPoint))
return o.equals(this); // ❌ 委託給 Point,破壞傳遞性
ColorPoint cp = (ColorPoint) o;
return super.equals(o) && cp.color == this.color;
}
}
// 測試
Point p = new Point(1, 2);
ColorPoint cp1 = new ColorPoint(1, 2, Color.RED);
ColorPoint cp2 = new ColorPoint(1, 2, Color.BLUE);
System.out.println(p.equals(cp1)); // true (Point 只比坐標)
System.out.println(cp1.equals(p)); // true (ColorPoint 委託 Point)
System.out.println(cp1.equals(cp2)); // false (顏色不同)
// 違反傳遞性:p=cp1, p=cp2 但 cp1≠cp2
結論:可實例化類不可繼承並重寫 equals,或使用組合代替繼承,或僅在 final 類中重寫。
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "area code");
this.prefix = rangeCheck(prefix, 999, "prefix");
this.lineNum = rangeCheck(lineNum, 9999, "line num");
}
@Override
public boolean equals(Object o) {
// 1. 自反性快速路徑
if (o == this) return true;
// 2. 類型檢查(用 instanceof 支持子類,但本類 final 所以等同 getClass())
if (!(o instanceof PhoneNumber)) return false;
// 3. 強轉
PhoneNumber pn = (PhoneNumber) o;
// 4. 關鍵字段逐個比較(優先比較差異度大、計算廉價的字段)
return pn.lineNum == lineNum
&& pn.prefix == prefix
&& pn.areaCode == areaCode;
}
@Override
public int hashCode() {
// 5. 必須重寫 hashCode!公式:31 * 前一結果 + 字段hash
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
// 輔助方法...
}
Alt+Insert → equals() and hashCode(),勾選所有非瞬態字段int、String、enum 比較快且無 NPE 風險NaN、-0.0 邊界EqualsVerifier 庫自動驗證五大契約| 誤區 | 正確做法 |
|---|---|
參數類型寫成 equals(MyClass o) | 必須 equals(Object o),否則是重載非覆寫 |
| 忘記重寫 hashCode | equals 重寫必重寫 hashCode,否則 Map/Set 失效 |
用 getClass() != o.getClass() 替代 instanceof | final 類可用;可繼承類須用 instanceof 保持多態性 |
直接比較浮點數 f1 == f2 | 用 Float.compare(f1, f2) == 0 |
集合字段直接 list1 == list2 | 用 list1.equals(list2) 比較內容 |
equals 定義業務邏輯上的「同一性」,核心服務於集合框架== 檢查 → instanceof → 強轉 → 關鍵字段比較 → hashCode 配套