T3 t = new T3(); // name="李楠"
Te3 te = new Te3(); // name="劉洋"
System.out.println(t.equals(te)); // true ← 父類別 equals 忽略子類別欄位
System.out.println(te.equals(t)); // false ← 子類別 equals 要求對方也是 Te3
輸出:
true
false
程式沒有拋出例外、沒有崩潰,JVM 也沒有攔截——**因為 Java 語言規範只規定「契約」,不強制執行**。
Object.equals 文檔明確要求:
a.equals(b) == b.equals(a)違反契約**不會導致編譯錯誤或執行期例外**,但會讓**依賴契約的集合框架行為失控**。
Set<T3> set = new HashSet<>();
set.add(t);
set.add(te); // 邏輯上應視為不同物件
System.out.println(set.size()); // 輸出 1 還是 2?視 hashCode 實作而定,極大機率出錯
System.out.println(set.contains(te)); // 可能回傳 false
因為 HashSet 先比 hashCode 再呼叫 equals,若 t.equals(te) != te.equals(t),集合內部判斷邏輯會產生矛盾,導致:
• 重複元素無法正確去重
• contains/remove 失效
• 迭代器行為不可預期
// 父類別 T3
public boolean equals(Object o) {
if (!(o instanceof T3)) return false; // 允許子類別實例通過
T3 other = (T3) o;
return name.equals(other.name) ...; // 只比較父類別欄位
}
// 子類別 Te3 extends T3
public boolean equals(Object o) {
if (!super.equals(o)) return false; // 先呼叫父邏輯
if (!(o instanceof Te3)) return false; // 但要求對方必須是 Te3
// 比較子類別欄位...
}
場景:
t.equals(te) → te instanceof T3 為 true → 只比父欄位 → 相同 → true
te.equals(t) → super.equals(t) 為 true → t instanceof Te3 為 false → false
// 父類別
@Override public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; // 嚴格同類別
T3 other = (T3) o;
return Objects.equals(name, other.name) ...;
}
優點:簡單、嚴格滿足對稱性與傳遞性。
缺點:子類別無法與父類別互為 equals(通常這正是想要的)。
參考 Effective Java Item 10:
1. 父類別 equals 宣告為 final,不允許子類別覆寫。
2. 子類別若需比較額外欄位,改用 **組合** 而非繼承,或提供 equalsExcludingSubclassFields 等輔助方法。
3. 真正需要多型比較時,定義介面 Equatable<T> 並在介面層級實作對稱比較邏輯。
「能跑」≠「正確」。equals 契約是集合框架正確運作的前提,違反對稱性如同在炸彈上跳舞——平時不炸,關鍵時刻(進 HashMap、做去重、序列化)炸給你看。
**鐵律:覆寫 equals 時,務必配合 hashCode,並用 getClass() 守住類別邊界,除非你有極充分理由且完整處理跨類別對稱性。**