覆蓋equals時遵守通用約定

1. 盡量避免覆蓋equals方法:

因為覆蓋equals方法看似很簡單,但實際上有許多覆蓋方式會導致錯誤,并且后果很嚴重。

2. 什么情況下,不需要覆蓋equals方法?
  • 類的每個實例本質上都是唯一的
    對于代表活動實體而不是值的類來說確實如此,比如Thread類。
  • 不關心類是否提供了“邏輯相等”的測試功能
    簡單來說,就是我們所設計的類不要去判斷實例之間是否相等。
  • 超類已經覆蓋了equals,從超類繼承過來的行為對于子類也是合適的
    比如,我們創建一個Man類,并定義其id(類似身份證)作為判斷是否為同一個Man對象。為了滿足需求,我們需要創建一個OldMan類繼承自Man類,這時候,OldMan中要判斷是否為同一對象,完全可以用繼承于父類的equals方法。
  • 類是私有的或是包是私有的,可以確定它的equals方法永遠不會被調用。
    在這種情況下,是需要覆蓋equals方法,以防止它被意外調用:
@Override
public boolean equals(Object o){
    throw new AssertionError();
}
3. 什么情況下需要覆蓋equals方法?

如果類具有自己特定的“邏輯相等”(不同于對象等同的概念),而且超類還沒有覆蓋equals以實現自己期望的行為,這時候我們就需要覆蓋equals方法。

比如,我們創建的Man類,沒有定義equals方法,當我們再創建其子類OldMan時,如果這時候需要判斷兩個OldMan對象是否相同,那么這時候OldMan就需要覆蓋equals方法。

4. 覆蓋equals方法所要遵守的通用約定:
  • 自反性:對于任何非null的引用值x, x.equals(x)必須返回true。
  • 對稱性:對于任何非null的引用值x和y,當且僅當y.equals(x)返回true時,x.equals(y)必須返回true。
  • 傳遞性:對于任何非null的引用值,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(y)也必須返回true。
  • 一致性:對于任何非null的引用值x和y,只要equals的比較操作在對象中所用的信息沒有被修改,多次調用x.equals(y)就會一致地返回true,或者一致地返回false。
  • 非空性:對于任何非null的引用值x,x.equals(null)必須返回false。
  • 自反性:這個應該很好理解,就是要滿足對象必須等于其本身,一般來說,是很少違反的。

  • 對稱性:我們看下一個關于字符串的例子

public class CaseInsensitiveString {
    private final 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;
    }

    public static void main(String[] args){
        CaseInsensitiveString cis = new CaseInsensitiveString("Test");
        String s = "test";
        System.out.println("cis.equals(s) : " + cis.equals(s));
        System.out.println("s.equals(cis) : " + s.equals(cis));
    }
}

輸出結果:

cis.equals(s) : true
s.equals(cis) : false

這顯然違背了equals的對稱性原則,我們來解析下覆蓋的equals方法:

    @Override
    public boolean equals(Object o){
        //如果傳入的對象為CaseInsensitiveString類型,則通過比較該類型的成員變量s來判斷對象是否相同
        if (o instanceof CaseInsensitiveString)
            return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
        //如果傳入的對象是String類型,則類中的成員變量與s直接比較
        if (o instanceof String)
            return s.equalsIgnoreCase((String) o);
        return false;
    }

實際上,cis.equals(s)原本應該為false,因為cis和s兩者本身就不是同一種類型,那么為什么會導致其返回的是true呢?原因就在于:使用cis對象中的成員變量s與字符串進行比較

 if (o instanceof String)
            return s.equalsIgnoreCase((String) o);

這種情況,只能說cis中的成員變量s和字符串s是相同的,但不能說cis對象和字符串s對象是相同的。

因此,要解決這個問題,實際上,很簡單,只要將后面那段刪除即可:

    @Override
    public boolean equals(Object o) {
        return o instanceof CaseInsensitiveString && s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
    }

輸出結果:

cis.equals(s) : false
s.equals(cis) : false
  • 傳遞性:

我們先創建一個Point類:

public class Point {
    private final int x;
    private final 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中x和y都相同才算為同一個對象。
接下來,我們需要擴展下這個類,為其添加顏色信息:

public class ColorPoint extends Point {
    enum Color{
        RED,BLACK,WHITE
    }
    private final  Color color;
    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        return super.equals(o) && ((ColorPoint) o).color == color;
    }
}

這時候,我們重寫了equals方法,因為如果繼續使用父類的equals方法,會忽略掉顏色信息。

好,我們來看下這樣重寫的equals方法是否正確:

public static void main(String[] args){
        Point p = new Point(1, 2);
        ColorPoint cp = new ColorPoint(1, 2 , Color.BLACK);
        System.out.println("p.equals(cp): " + p.equals(cp));
        System.out.println("cp.equals(p): " + cp.equals(p));
    }

輸出結果:
p.equals(cp): true
cp.equals(p): false

我們可以看到這明顯違反了"自反性“原則,為什么會產生這樣的結果呢?

實際上,p.equals(cp)調用的是父類的方法,比較的是點的x和y,兩者x和y都相同,自然返回true,而cp.equals(p)調用的是子類的equals方法,由于傳入的p并不是CorlorPoint類型,因此直接返回false。

那么如何讓p和cp比較時可以忽略”顏色信息“,但又不會違反”自反性“呢?

    @Override
    public boolean equals(Object o) {
        //不屬于Point類和ColorPoint類的情況
        if(!(o instanceof Point))
            return false;
        //屬于Point類的情況
        if (!(o instanceof ColorPoint))
            return o.equals(this);
        //屬于ColorPoint類的情況
        return super.equals(o) && ((ColorPoint) o).color == color;
    }

    public static void main(String[] args){
        Point p = new Point(1, 2);
        ColorPoint cp = new ColorPoint(1, 2 , Color.BLACK);
        System.out.println("p.equals(cp): " + p.equals(cp));
        System.out.println("cp.equals(p): " + cp.equals(p));
        System.out.println("cp instanceof Point: " + (cp instanceof Point));
    }

輸出結果:
p.equals(cp): true
cp.equals(p): true
cp instanceof Point: true

我們可以看到”自反性“的問題已經解決了,這里要注意的一點是: 子類instanceof父類 一定是true的。

那么,這樣的修改是否就完美了呢?我們再進行一個小測試:

public static void main(String[] args){
        ColorPoint p1 = new ColorPoint(1, 2 , Color.BLACK);
        Point p2 = new Point(1, 2);
        Point p3 = new ColorPoint(1, 2 , Color.WHITE);
        System.out.println("p1.equals(p2): " + p1.equals(p2));
        System.out.println("p2.equals(p3): " + p2.equals(p3));
        System.out.println("p1.equals(p3): " + p1.equals(p3));
    }

輸出結果:
p1.equals(p2): true
p2.equals(p3): true
p1.equals(p3): false

根據”傳遞性“,p1.equals(p3)應該為true,而這里為false,明顯違反了該原則。實際上,根據ColorPoint中的equals方法,這個結果很明顯是false,因為p1和p3同為ColorPoint類型,因此,返回return super.equals(o) && ((ColorPoint) o).color == color;,兩者x和y相同,但color是不同的,因此為false。

那么如何解決”傳遞性“問題呢?

實際上,這是面向對象語言中關于等價關系的一個基本問題。我們無法在擴展可實例化的類的同時,既增加新的值組件,同時又保留equals約定。

雖然沒有一種很好的辦法可以既擴展不可實例化的類,又增加值組件,但有一種不錯的權宜之計:根據原則”復合優先于繼承“,我們不再讓ColorPoint擴展Point,而是在ColorPoint中加入一個私有的point域。

public class ColorPoint{
    enum Color{
        RED,BLACK,WHITE
    }
    private final  Color color;
    private final Point point;
    public ColorPoint(int x, int y, Color color) {
        this.color = color;
        point = new Point(x, y);
    }

    public Point asPoint(){
        return point;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint) o;
        return cp.point.equals(point) && cp.color.equals(color);
    }

    public static void main(String[] args){
        ColorPoint p1 = new ColorPoint(1, 2 , Color.BLACK);
        Point p2 = new Point(1, 2);
        ColorPoint p3 = new ColorPoint(1, 2 , Color.WHITE);
        System.out.println("p1.equals(p2): " + p1.equals(p2));
        System.out.println("p2 equals(p1): " + p2.equals(p1));
        System.out.println("p2.equals(p3): " + p2.equals(p3));
        System.out.println("p1.equals(p3): " + p1.equals(p3));
    }
    
}

輸出結果:
p1.equals(p2): false
p2 equals(p1): false
p2.equals(p3): false
p1.equals(p3): false

如此,我們便解決了”自反性“和”傳遞性“問題了。

  • 一致性:如果兩個對象相等,它們就必須始終相等,除非它們中有一個對象(或兩個都)被修改了。

  • 非空性:所有的對象都必須不等于null。因此,我們在覆蓋equals方法時,必須先檢查其正確類型。

@Override
public boolean equals(Object o){
      if (! ( o instanceof MyType))
              return false;
      MyType mt = (MyType) o;
....

如果漏掉這一步的類型檢查,并且傳遞給equals方法的參數又是錯誤的類型,那么equals方法將會拋出ClassCastException異常。

覆蓋equals方法的建議:
  • 使用==操作符檢查 ”參數是否為這個對象的引用“。
  • 使用instanceof操作符檢查 ”參數是否為正確的類型“。
  • 把參數轉換為正確的類型。
  • 對于該類中的”關鍵域“,檢查參數中的域是否與對象中對應的域相匹配。
  • 當完成equals方法后,要檢查是否滿足四個特性,最好測試檢查。
  • 覆蓋equals時總要覆蓋hashCode
  • 不要企圖讓equals方法過于智能
  • 不要將equals聲明中的Object對象替換為其他的類型。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容