equals方法的覆蓋(Override)看起來很簡單,但是許多的覆蓋方式都是錯誤的,將導致非常嚴重的後果。規避這類後果的最簡單的方法就是不覆蓋equals方法,如果滿足以下任一條件,就不需要覆蓋equals方法:
- 類的每一個實例本質上都是唯一的。對於大部分非值類(value class)的實例來說,Object類提供的equals實現都是正確的。
- 不關心類是否提供了“邏輯相等(logical equality)”的比較功能。
- 父類已經覆蓋了equals方法,且父類的equals實現對於子類也是適用的。
- 類是私有的或者是包級私有的,可以確定其equals方法永遠不會被調用。在這種情況下,爲了防止equals方法被意外調用,可以覆蓋equals方法並拋出錯誤:
@Override
public boolean equals(Object o) {
throw new AssertionError();
}
equals方法的核心意義是實現了等價關係(equivalence relation),在覆蓋equals方法時,必須遵守以下規範:
- 非空性(non-nullity) :對於任何非null的引用值x,x.equals(null)必須返回false。
- 自反性(reflexive) :對於任何非null的引用值x,x.equals(x)必須返回true。
- 對稱性(symmetric) :對於任何非null的引用值x和y,當且僅當y.equals(x)返回true時,x.equals(y)必須返回true。
- 傳遞性(transitive) :對於任何非null的引用值x、y和z,如果x.equals(y)返回true,且y.equals(z)也返回true,那麼x.equals(z)也必須返回true。
- 一致性(consistent) :對於任何非null的引用值x和y,只要equals方法所用到的對象的信息沒有發生改變,多次調用x.equals(y)返回的結果就會保持一致。
如果違反了以上五條規範,程序將在某些時候表現不正常,甚至崩潰,而且很難找到失敗的根源。沒有哪個類是孤立的,一個類的實例通常會被頻繁地傳遞給另一個類的實例。有許多類,包括所有的集合類(collection class)在內,都依賴於傳遞給他們的對象是否遵守了equals的使用規範。
下面,逐一討論下這幾條規範:
非空性是指所有的對象都必須不等於null,一旦出現對象爲空的情況,雖然很難出現o.equals(null)意外返回true的情況,但很可能意外拋出NullPointerException異常。許多類的equals方法通過顯示的null判斷來防止這種情況:
@Override
public boolean equals(Object o) {
if (null == o)
return false;
...
}
這個判斷是不必要的,在使用instanceof操作符檢查參數類型時就已經實現了null判斷功能:
@Override
public boolean equals(Object o) {
if (! (o instanceof MyType))
return false;
MyType type = (MyType) o;
...
}
自反性是指對象必須等於自身,這一條默認是自動滿足的,除非強制進行錯誤的處理。加入違背了這一條,然後把該類的實例添加到集合類中,該集合的contains方法將查詢不到添加的實例。
對稱性是指任何兩個對象對於“它們是否相等”的判斷都必須保持一致,違反這一條規定的情況使比較很容易出現的。例如,下面的類實現了一個不區分大小寫的字符串。
public final class CaseInsensitiveString{
private String s;
public CaseInsensitiveString(String s){
if(s == null)
throw new NullPointerException;
this.s = s;
}
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(((CaseInsensitiveString)o).s);
if(o instanceof String)
return s.equalsIgnoreCase((String)o);
return false;
...
}
}
在這個類中,equals方法實現了與普通字符串對象進行互操作,假設有一個不區分大小寫的字符串和一個普通字符串:
CaseInsensitiveString cis = new CaseInsensitiveString("Csdn");
String s = "csdn";
這種情況下,cis.equals(s)將返回true,但是s.equals(cis)卻返回false,這顯然違背了對稱性。這是因爲String類中的equals方法並不知道CaseInsensitiveString。一旦違反了equals規範,當其他對象面對你的對象時,你完全不知道這些對象的行爲會怎樣。
傳遞性是指如果一個對象等於第二個對象,並且第二個對象又等於第三個對象,則第一個對象一定等於第三個對象。當子類增加的信息會影響到equals的比較結果時,就容易違反這一規範,下面以Point類爲例進行講解:
public class Point{
private int x;
private int y;
public Point(int x,int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if(!(o instanceof Point))
return false;
Point p = (Point)o;
return p.x == x && p.y ==y;
}
}
現在擴展Point類,添加顏色信息:
public class ColorPoint extends Point{
private final Color color;
public Point(int x,int y,Color color) {
super(x,y);
this.color = color;
}
@Override
public boolean equals(Object o) {
if(!(o instanceof Point))
return false;
if(!(o instanceof ColorPoint))
return o.equals(this);
return super.equals(o)&&((ColorPoint)o).color == color;
}
}
假設,現在有如下三個對象:
ColorPoint p1 = new ColorPoint(1,1,Color.RED);
Point p2 = new Point(1,1);
ColorPoint p3 = new ColorPoint(1,1,Color.BLUE);
此時,p1.equals(p2)和p2.equals(p3)都返回true,但是p1.equals(p3)則是返回false,很顯然違反了傳遞性。這是面嚮對象語言中關於等價問題的一個基本問題。我們無法在擴展(extends)可實例化的類的同時,既增加新的值組件(value component),同時又保留equals規範。雖然沒有一種令人滿意的辦法可以既擴展可實例化的類,又增加值組件,但是還是有一種選擇,即通過複合(composition)的方式實現(本文不再展開)。另外,可以在一個抽象類(abstract
class)的子類中增加新的值組件,而不違反equals規範,也就是說,只要可能直接創建父類的實例,前面說的問題就不會發生。一致性是指對於可變對象,如果兩個對象相等,他們就必須始終相等,除非它們中至少有一個對象被修改了。對於不可變對象,就必須保證equals方法滿足這樣的限制條件:相等的對象永遠相等,不相等的對象永遠不相等。無論類是否是不可變的,都不要使equals方法依賴於不可靠的資源。如果違反了這一條,要滿足一致性的要求就十分困難了。除了極少數的例外情況,equals方法都應該對駐留在內存中的對象執行確定性的計算。
以上,詳細地討論了equals方法的使用規範,現在梳理幾條實現高質量equals方法的步驟和訣竅:
- 使用==操作符檢查“參數是否爲這個對象自身的引用”。
- 使用instanceof操作符檢查“參數是否爲正確的類型”。
- 把參數轉換成正確的類型。
- 對於該類中的每個“關鍵”域,檢查參數中的域是否與該對象中對應的域相匹配。
- 完成equals方法編寫後,編寫單元測試來檢驗equals方法是否滿足對稱性、傳遞性、一致性(自反性和非空性通常會自動滿足)。
- 覆蓋equals方法時總要覆蓋hashCode方法;
- 不要企圖讓equals方法過於智能;
- 不要將equals方法聲明中的Object對象替換爲其他的類型:這樣是重載(Overload)了Object.equals方法,並沒有實現equals方法的覆蓋,增加了代碼的複雜性。