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對象替換為其他的類型。