Effective Java 3e 讀書心得 - Item 10:Obey the general contract when overriding equals
覆載 equals()
的場景
非常多的情境下,並不值得覆載(override) equals()
重新定義物件間「相等」的邏輯
比方說:
- 該類別個別物件本質上是不同的
- 沒有提供「相等」比較的必要,比方說 java.util.regex.Pattern
- 父類別已經覆載
equals()
,並且其邏輯用在這個類別是合理的 - 該類別僅在這個專案內使用,並且你很確定專案內沒有用到
equals()
如果遇到以上情境,不要覆載 equals()
就可以避免掉很多問題。
不過,還是有一些狀況,我們會需要 覆載 equals()
根據書中提到,當一個類別邏輯上的「相等」已經和物件的相同有所差距,這時就很值得為其定義一個客製化的 equals()
It is when a class has a notion of logical equality that differs from mere object identity and a superclass has not already overridden equals.(p. 38)
覆載 equals()
的慣例
即便如此,「相等」這件事情必須要遵守一些慣例:
- 自反性(Reflexive):對任意非空值 x,x.equals(x) 應為 true
- 對稱性(Symmetric):對任意非空值 x、y,x.equals(y) 應等於 y.equals(x)
- 遞移性(Transitive):對任意非空值 x、y、z,如果 x.equals(y) 為 true 且 y.equals(z) 為 true,則 x.equals(z) 應為 true
- 一致性(Consistent):對任意非空值 x、y,只要不更動這裡面的值,無論呼叫幾次,x.equals(y) 的回傳要一樣
- 對任意非空值 x 來說,x.equals(null) 要等於 false
違反這些慣例,會導致其他行為變得難以預測
違反慣例的問題
書中相關篇幅不少
以下舉其中一個違反慣例可能導致的問題
違反對稱性
這個狀況書中提到了一個可能的案例
比方說,你可能覺得每次比對大小寫無關的字串時
還要全部轉成小寫再進行比對很不方便
所以寫了一個類別 CaseInsensitiveString
並覆載 equals
如下
// Broken - violates symmetry!
@Override public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(
((CaseInsensitiveString) o).s);
if (o instanceof String) // One-way interoperability!
return s.equalsIgnoreCase((String) o);
return false;
}
這樣的設計乍看之下很方便
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
cis.equals(s) // true
但是卻違背了對稱性
cis.equals(s) // true
s.equals(cis) // false
違反這個特性,會導致其他使用 equals
的其他函數
比方說 Collection 的 contains
函數
無法正確判斷該物件是否存在於集合內
要修正這個問題
要改變我們一開始 CaseInsensitiveString
和 String
相同的想法
讓 CaseInsensitiveString
只能和 CaseInsensitiveString
相同
改寫 equals
如下
@Override public boolean equals(Object o) {
return o instanceof CaseInsensitiveString &&
((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
如何撰寫有效率的 equals()
針對怎麼撰寫一個有效率的 equals()
書中提出了以下建議
- Use the == operator to check if the argument is a reference to this object.
- Use the instanceof operator to check if the argument has the correct type.
- Cast the argument to the correct type.
- For each “significant” field in the class, check if that field of the argument matches the corresponding field of this object.
實作案例
Kotlin 官方已經提供我們一個很好的案例:data class!
data class 的 equals()
假設我們有一個 data class 如下
data class Customer(
val name: String,
val email: String
)
如果我們反組譯這個 data class 編譯出的 bytecode
可以看到裡面定義了客製版本的 equals()
,實作如下
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
} else if (!(other instanceof Customer)) {
return false;
} else {
Customer var2 = (Customer)other;
if (!Intrinsics.areEqual(this.name, var2.name)) {
return false;
} else {
return Intrinsics.areEqual(this.email, var2.email);
}
}
}
首先,利用 ==
快速檢查物件是否是同一個參照(reference)
再來,快速檢查型態,如果型態不同則回傳 false
接著就是客製化的定義:即便兩個 Customer
物件可能是不同參照
不過如果他們有相同的名字和 email,我們就視為相等的 Customer
這件事情也可以看出 Kotlin 在實作上
確實很多地方參考了 Effective Java 這本書的觀念
作為其程式設計的架構
回到首頁