如何正確使用equals方法?

    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方法的步驟和訣竅:

  1. 使用==操作符檢查“參數是否爲這個對象自身的引用”。
  2. 使用instanceof操作符檢查“參數是否爲正確的類型”。
  3. 把參數轉換成正確的類型。
  4. 對於該類中的每個“關鍵”域,檢查參數中的域是否與該對象中對應的域相匹配。
  5. 完成equals方法編寫後,編寫單元測試來檢驗equals方法是否滿足對稱性、傳遞性、一致性(自反性和非空性通常會自動滿足)
    最後是幾條覆蓋equals方法時的注意點:
  • 覆蓋equals方法時總要覆蓋hashCode方法;
  • 不要企圖讓equals方法過於智能;
  • 不要將equals方法聲明中的Object對象替換爲其他的類型:這樣是重載(Overload)了Object.equals方法,並沒有實現equals方法的覆蓋,增加了代碼的複雜性。
發佈了41 篇原創文章 · 獲贊 1 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章