Java equals

Java中用equals方法來判斷兩個對象是不是相等,equals方法是Object類就擁有的方法,因而屬於所有對象通用的方法,使用方式很簡單:a.equals(b) ,返回true或false。下面進入正題。

什麼時候才應該覆蓋equals方法
我們都知道,如果不覆蓋equals方法,那麼就是使用的父類的equals方法,我們可以來看看Object的equals方法都做了什麼:

public boolean equals(Object obj) {  
    return (this == obj);  
}  

顯然,Object只是使用==運算符,簡單地判斷兩個對象是不是同一個對象,也就是說,new出來的兩個對象,不管他們屬性是不是相同,都是不相等的。而實際使用中,我們常常會碰到“邏輯相等”的需求,比如,我們認爲兩個半徑相同的圓,他們是相等的,這個時候,如果圓的父類,還沒有覆蓋equals方法實現這個邏輯相等,那麼,就需要在類裏面去覆蓋equals方法。
總結一下:如果類具有自己特有的“邏輯相等”概念,而且父類還沒有覆蓋equals方法實現期望的邏輯,這時候就需要我們覆蓋equals方法。

equals方法的五條約定
由於在很多集合的內部方法中,都會使用到equals方法,比如contains方法,因此我們在覆蓋equals方法的時候,需要遵循以下規定,否則會造成異常。
對於不等於null的x、y、z,有以下規定:

自反性 x.equals(x)==true。
對稱性 y.equals(x)==true <—> x.equals(y)==true。 通過下一節的例子你可以加深這條約定的理解。
傳遞性 x.equals(y)==true,y.equals(z)==true —> x.equals(z)==true
一致性 對於不等於null的x和y,只要對象中,被equals操作使用的信息沒有被修改,那麼多次調用x.equals(y),要麼一直返回true,要麼一直返回false。
要想嚴格遵循這一條約定,必須保證equals方法裏面不依賴外部不可靠的資源,如果equals方法裏面依賴了外部的資源,就很難保證具有一致性了。
非空性

對於不等於null的x,x.equals(null)必須返回false。要遵守這個約定,只需要在equals裏面加上這麼一段代碼 if(o == null) return false,然而,這樣做往往是沒有必要的,因爲我們都會在equals方法的第一步,做instanceof校驗,檢查參數是否爲正確的類型。而a.instanceOf(null)會返回false。

一個簡單的例子 告訴你爲什麼要遵守約定
假設有一個類,持有一個String對象,並且在比較時不區分大小寫,代碼如下,重點關注一下它的equals方法實現:

public final class CaseInsensitiveString {
private final String s;

public CaseInsensitiveString(String s) {  
    if (s == null)  
        throw new NullPointerException();  
    this.s = s;  
}  

// 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;  
}   }

equals方法這樣寫的出發點是非常好的,它試圖提供和普通String類型進行比較的能力,然而,它卻違反了對稱性的約定,因爲String不知道有這個類,運行下面這段代碼:

public static void main(String[] args) {
CaseInsensitiveString cis = new CaseInsensitiveString(“Polish”);
String s = “polish”;
System.out.println(cis.equals(s) + ” ” + s.equals(cis));//true false
}

cis.equals(s)==true,但是s.equals(cis)==false,違反了對稱性的規定,而這會造成什麼危害呢? 看看下面這段代碼,你覺得會打印出true還是false?
[java] view plain copy
public static void main(String[] args) {
CaseInsensitiveString cis = new CaseInsensitiveString(“Polish”);
String s = “polish”;

List<CaseInsensitiveString> list = new ArrayList<>();  
list.add(cis);  
System.out.println(list.contains(s));  

}

由於equals方法不符合對稱性的約定,因此打印true還是false,取決於ArrayList方法對contains方法的實現,如果他內部實現是cis.equals(s),那麼會返回true,如果是s.equals(cis),那麼會返回false。
看ArrayList的實現:
[java] view plain copy
public boolean contains(Object o) {
return indexOf(o) >= 0;
}

[java] view plain copy
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}

可以看出,根據ArrayList的內部實現,contains方法最終會執行這樣的代碼:s.equals(cis),所以他會返回false。
所以,如果equals方法違反了約定,很多行爲的結果將不可預知。

實現equals方法的訣竅
結合這五個約定,我們總結一下實現高質量equals方法的訣竅:
1. 使用==檢查參數是否爲這個對象的引用,是,則直接返回true,提供判斷的效率。
2. 使用instanceof檢查參數是否爲正確的類型,如果不是,返回false。
3. 把參數轉成正確的類型。因爲在第二步已經做了instanceof校驗,所以能夠確保這一步不會出錯。
4. 對於類中每一個關鍵的屬性,也就是“邏輯相等”需要判斷的屬性,逐一比較參數的這些屬性是否和對象的一致。比較時需要注意一下細節:
1) float和double類型的屬性,要使用Float.compare(f1,f2)或者Double.compare(d1,d2)方法進行比較
2) 對象類型的屬性,使用equals方法比較,有些屬性可能爲null,爲了避免出現空指針異常,可以採用這樣的方式:
(field ==null ? o.field == null : field.equlas(o.field))
3) 對於數組類型的屬性,則要把這些原則用到每個元素上,如果每個元素都很重要,可以考慮使用Arrays.equals方法
4) 比較順序會影響equals方法的性能,爲了獲得最佳的性能,應該最先比較最有可能不一致的屬性或者開銷最低的屬性。
5. 當你編寫完equals方法後,請檢查是否符合五條軍規——自反、對稱、傳遞、一致、非空,寫單元測試校驗!

其他注意事項

覆蓋了equals方法之後一定要覆蓋hashcode方法
在專欄的另一篇文章裏,我做了解釋,爲什麼覆蓋了equals方法之後一定要覆蓋hashCode方法?
不要把equals方法的入參類型改爲非Object的
很多人喜歡這樣做:
public boolean equals(MyClass o) …
問題在於,這個方法根本沒有覆蓋equals方法。

總結
因爲有邏輯相等的判斷需要,所以在父類沒有覆蓋的情況下,我們才需要覆蓋equals方法。
編寫equals方法需要遵循五條軍規:自反、對稱、傳遞、一致、非空。
很多Java提供的接口和類都調用了equals方法,不符合軍規的equals方法,會造成很多函數的結果不可預知。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章