天天看點

Effective Java - 對于所有對象都通用的方法 - 覆寫 equals 時請遵守通用約定

讀書筆記 僅供參考

不覆寫 equals 方法

許多覆寫方式會導緻錯誤,并且後果十分嚴重,最容易避免錯誤的方法就是不覆寫 equals 方法。每個類的執行個體都隻與自身相等。

  • 類的每個執行個體本質上都是唯一的
  • 不關心類是否提供了“邏輯相等”的功能
  • 超類已經覆寫了 equals,從超類繼承過來的行為對子類也是合适的(例如 List 從 AbstractList 繼承 equals)
  • 如果類是私有的或包級私有,可以覆寫equals 方法,確定它的 equals 方法不會被意外調用
@Override
public boolean equals(Object o){
    throw new AssertionError();
}
           

何時覆寫 equals

如果類具有自己特有的“邏輯相等”概念,而且超類還沒有覆寫 equals 。

這種類屬于值類,例如 Integer 或 Data,隻是想知道他們在邏輯上相等,并不像知道他們是否指向同一個對象。(如果是執行個體受控的值類,可以不覆寫 equals,因為每個值至多存在一個對象)

覆寫 equals 的通用規定

如果違反了這些規定,程式會表現不正常,甚至崩潰。

自反性

對于任何非 null 的引用值 x,x.equals(x) 必須傳回 true。

對稱性

對于任何非 null 的引用值 x 和 y,并且僅當 y.equals(x) 傳回 true 時,x.equals(y) 必須傳回 true。

錯誤的例子

//實作不區分大小寫的字元串
public final 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;
    }
}
           
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
cis.equals(s);//傳回 true
//String 并不知道如何比較
s.equals(cis);//傳回 false
           

解決方法:隻比較 CaseInsensitiveString

傳遞性

對于任何非 null 的引用值 x,y 和 z,如果 x.equals(y) 傳回 true,并且 y.equals(z) 傳回 true,x.equals(z) 必須傳回 true。

在考慮子類的情況下,這條約定很容易違背。

超類

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

子類

public class ColorPoint extends Point {
    private final Color color;
    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }
    //如果不提供 equals 方法,顔色資訊就忽略掉了
    @Override
    public boolean equals(Object o) {
        if(!(o instanceof ColorPoint))
            return false;
        return super.equals(o) && ((ColorPoint) o).color == color;
    }
}
           

在比較普通點和有色點,以及相反的情形時,會得到不同的結果,違反了對稱性。

Point p = new Point(, );
ColorPoint cp =new ColorPoint(, , Color.RED);
p.equals(cp);//傳回 true
cp.equals(p);//傳回 false
           

改善版本

@Override
    public boolean equals(Object o) {
        if(!(o instanceof ColorPoint))
            return false;
        //是一個普通點,就忽略掉顔色資訊
        if(!(o instanceof ColorPoint))
            return o.equals(this);
        //是一個彩色點,就全比較
        return super.equals(o) && ((ColorPoint) o).color == color;
    }
           

這種方法提供了對稱性,缺失了傳遞性

ColorPoint p1 = new ColorPoint(, , Color.RED);
Point p2 = new Point(, );
ColorPoint p3 = new ColorPoint(, , Color.BLUE);
p1.equals(p2);//傳回 true
p2.equals(p3);//傳回 true
p1.equals(p3);//傳回 false
           

我們無法在擴充可執行個體化的類的同時,既增加新的值元件,同時又保留 equals 約定

聽說:在 equals 方法中用 getClass 代替 instanceof ,可以滿足上面的要求

//Point 類
    @Override
    public boolean equals(Object o) {
        if(o == null || o.getClass() != getClass()) {
            return false;
        }
        Point p = (Point) o;
        return p.x == x && p.y ==y;
    }
           

但是這樣違反了 裡氏替換原則:一個類型的任何重要屬性也将适用于它的子類型。當遇到類似 HashSet 的集合時,就無法将超類和子類都放進去了。

比較好的方法

采用複合,在 ColorPoint 中加入 Point 屬性。

public class ColorPoint {
    private final Point point;
    private final Color color;
    public ColorPoint(int x, int y, Color color) {
        if(color ==null) {
            throw new NullPointerException();
        }
        point = new Point(x, y);
        this.color = color;
    }

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

ps:抽象類的子類可以增加新的值元件,而不會違反 equals 約定。

一緻性

對于任何非 null 的引用值 x 和 y,隻要 equals 的比較操作在所用的資訊沒有被改變,多次調用 x.equals(y) 就會一直傳回同一個結果

非空性

對于任何非 null 的引用值 x,x.equals(null) 必須傳回 false

實作 equals 方法的訣竅

訣竅一

使用 == 操作符檢查 “參數是否是這個對象的引用”

訣竅二

使用 instanceof 操作符 檢查 “參數是否為正确的類型”

訣竅三

把參數轉換成正确的類型。

在轉化之前進行 instanceof ,是以肯定會成功

訣竅四

對于該類中的每個“關鍵域”,檢查是否相比對

float : Float.compare()

double:Double.compare()

如果某些引用域包含 null 合法:

(field == null? o.field == null : field.equals(o.field))

如果通常是相同的對象引用:

(field == o.field || (filed != null && field.equals(o.field)))

提高性能:

先比較最有可能不一緻的域

訣竅五

當 equals 完成後,要問自己:是否對稱,傳遞,一緻?

告誡

  • 覆寫 equals 時總要覆寫 hashCode
  • 不要企圖讓 equals 方法過于智能
  • 不要将 equals 聲明的 Object 對象轉換為其他的類型