讀書筆記 僅供參考
不覆寫 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 對象轉換為其他的類型