1. 盡量避免覆蓋equals方法:
因?yàn)楦采wequals方法看似很簡(jiǎn)單迂卢,但實(shí)際上有許多覆蓋方式會(huì)導(dǎo)致錯(cuò)誤恢着,并且后果很嚴(yán)重茶宵。
2. 什么情況下危纫,不需要覆蓋equals方法?
- 類的每個(gè)實(shí)例本質(zhì)上都是唯一的乌庶。
對(duì)于代表活動(dòng)實(shí)體而不是值的類來(lái)說(shuō)確實(shí)如此种蝶,比如Thread類。
- 不關(guān)心類是否提供了“邏輯相等”的測(cè)試功能瞒大。
簡(jiǎn)單來(lái)說(shuō)蛤吓,就是我們所設(shè)計(jì)的類不要去判斷實(shí)例之間是否相等。
- 超類已經(jīng)覆蓋了equals糠赦,從超類繼承過(guò)來(lái)的行為對(duì)于子類也是合適的会傲。
比如,我們創(chuàng)建一個(gè)Man類拙泽,并定義其id(類似身份證)作為判斷是否為同一個(gè)Man對(duì)象淌山。為了滿足需求,我們需要?jiǎng)?chuàng)建一個(gè)OldMan類繼承自Man類顾瞻,這時(shí)候泼疑,OldMan中要判斷是否為同一對(duì)象,完全可以用繼承于父類的equals方法荷荤。
- 類是私有的或是包是私有的退渗,可以確定它的equals方法永遠(yuǎn)不會(huì)被調(diào)用。
在這種情況下蕴纳,是需要覆蓋equals方法会油,以防止它被意外調(diào)用:
@Override
public boolean equals(Object o){
throw new AssertionError();
}
3. 什么情況下需要覆蓋equals方法?
如果類具有自己特定的“邏輯相等”(不同于對(duì)象等同的概念)古毛,而且超類還沒有覆蓋equals以實(shí)現(xiàn)自己期望的行為翻翩,這時(shí)候我們就需要覆蓋equals方法。
比如稻薇,我們創(chuàng)建的Man類嫂冻,沒有定義equals方法,當(dāng)我們?cè)賱?chuàng)建其子類OldMan時(shí)塞椎,如果這時(shí)候需要判斷兩個(gè)OldMan對(duì)象是否相同桨仿,那么這時(shí)候OldMan就需要覆蓋equals方法。
4. 覆蓋equals方法所要遵守的通用約定:
- 自反性:對(duì)于任何非null的引用值x, x.equals(x)必須返回true案狠。
- 對(duì)稱性:對(duì)于任何非null的引用值x和y服傍,當(dāng)且僅當(dāng)y.equals(x)返回true時(shí)暇昂,x.equals(y)必須返回true。
- 傳遞性:對(duì)于任何非null的引用值伴嗡,如果x.equals(y)返回true急波,并且y.equals(z)返回true,那么x.equals(y)也必須返回true瘪校。
- 一致性:對(duì)于任何非null的引用值x和y澄暮,只要equals的比較操作在對(duì)象中所用的信息沒有被修改,多次調(diào)用x.equals(y)就會(huì)一致地返回true阱扬,或者一致地返回false泣懊。
- 非空性:對(duì)于任何非null的引用值x,x.equals(null)必須返回false麻惶。
自反性:這個(gè)應(yīng)該很好理解馍刮,就是要滿足對(duì)象必須等于其本身,一般來(lái)說(shuō)窃蹋,是很少違反的卡啰。
對(duì)稱性:我們看下一個(gè)關(guān)于字符串的例子
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));
}
}
輸出結(jié)果:
cis.equals(s) : true
s.equals(cis) : false
這顯然違背了equals的對(duì)稱性原則,我們來(lái)解析下覆蓋的equals方法:
@Override
public boolean equals(Object o){
//如果傳入的對(duì)象為CaseInsensitiveString類型警没,則通過(guò)比較該類型的成員變量s來(lái)判斷對(duì)象是否相同
if (o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
//如果傳入的對(duì)象是String類型匈辱,則類中的成員變量與s直接比較
if (o instanceof String)
return s.equalsIgnoreCase((String) o);
return false;
}
實(shí)際上,cis.equals(s)原本應(yīng)該為false杀迹,因?yàn)閏is和s兩者本身就不是同一種類型亡脸,那么為什么會(huì)導(dǎo)致其返回的是true呢?原因就在于:使用cis對(duì)象中的成員變量s與字符串進(jìn)行比較
if (o instanceof String)
return s.equalsIgnoreCase((String) o);
這種情況树酪,只能說(shuō)cis中的成員變量s和字符串s是相同的浅碾,但不能說(shuō)cis對(duì)象和字符串s對(duì)象是相同的。
因此续语,要解決這個(gè)問(wèn)題垂谢,實(shí)際上,很簡(jiǎn)單绵载,只要將后面那段刪除即可:
@Override
public boolean equals(Object o) {
return o instanceof CaseInsensitiveString && s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
}
輸出結(jié)果:
cis.equals(s) : false
s.equals(cis) : false
- 傳遞性:
我們先創(chuàng)建一個(gè)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都相同才算為同一個(gè)對(duì)象苛白。
接下來(lái)娃豹,我們需要擴(kuò)展下這個(gè)類,為其添加顏色信息:
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;
}
}
這時(shí)候购裙,我們重寫了equals方法懂版,因?yàn)槿绻^續(xù)使用父類的equals方法,會(huì)忽略掉顏色信息躏率。
好躯畴,我們來(lái)看下這樣重寫的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));
}
輸出結(jié)果:
p.equals(cp): true
cp.equals(p): false
我們可以看到這明顯違反了"自反性“原則民鼓,為什么會(huì)產(chǎn)生這樣的結(jié)果呢?
實(shí)際上蓬抄,p.equals(cp)調(diào)用的是父類的方法丰嘉,比較的是點(diǎn)的x和y,兩者x和y都相同,自然返回true嚷缭,而cp.equals(p)調(diào)用的是子類的equals方法饮亏,由于傳入的p并不是CorlorPoint類型,因此直接返回false阅爽。
那么如何讓p和cp比較時(shí)可以忽略”顏色信息“路幸,但又不會(huì)違反”自反性“呢?
@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));
}
輸出結(jié)果:
p.equals(cp): true
cp.equals(p): true
cp instanceof Point: true
我們可以看到”自反性“的問(wèn)題已經(jīng)解決了付翁,這里要注意的一點(diǎn)是: 子類instanceof父類 一定是true的简肴。
那么,這樣的修改是否就完美了呢百侧?我們?cè)龠M(jìn)行一個(gè)小測(cè)試:
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));
}
輸出結(jié)果:
p1.equals(p2): true
p2.equals(p3): true
p1.equals(p3): false
根據(jù)”傳遞性“砰识,p1.equals(p3)應(yīng)該為true,而這里為false佣渴,明顯違反了該原則仍翰。實(shí)際上,根據(jù)ColorPoint中的equals方法观话,這個(gè)結(jié)果很明顯是false予借,因?yàn)閜1和p3同為ColorPoint類型,因此频蛔,返回return super.equals(o) && ((ColorPoint) o).color == color;
,兩者x和y相同灵迫,但color是不同的,因此為false晦溪。
那么如何解決”傳遞性“問(wèn)題呢瀑粥?
實(shí)際上,這是面向?qū)ο笳Z(yǔ)言中關(guān)于等價(jià)關(guān)系的一個(gè)基本問(wèn)題三圆。我們無(wú)法在擴(kuò)展可實(shí)例化的類的同時(shí)狞换,既增加新的值組件,同時(shí)又保留equals約定舟肉。
雖然沒有一種很好的辦法可以既擴(kuò)展不可實(shí)例化的類修噪,又增加值組件,但有一種不錯(cuò)的權(quán)宜之計(jì):根據(jù)原則”復(fù)合優(yōu)先于繼承“路媚,我們不再讓ColorPoint擴(kuò)展Point黄琼,而是在ColorPoint中加入一個(gè)私有的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));
}
}
輸出結(jié)果:
p1.equals(p2): false
p2 equals(p1): false
p2.equals(p3): false
p1.equals(p3): false
如此整慎,我們便解決了”自反性“和”傳遞性“問(wèn)題了脏款。
一致性:如果兩個(gè)對(duì)象相等围苫,它們就必須始終相等,除非它們中有一個(gè)對(duì)象(或兩個(gè)都)被修改了撤师。
非空性:所有的對(duì)象都必須不等于null剂府。因此,我們?cè)诟采wequals方法時(shí)剃盾,必須先檢查其正確類型周循。
@Override
public boolean equals(Object o){
if (! ( o instanceof MyType))
return false;
MyType mt = (MyType) o;
....
如果漏掉這一步的類型檢查,并且傳遞給equals方法的參數(shù)又是錯(cuò)誤的類型万俗,那么equals方法將會(huì)拋出ClassCastException異常湾笛。
覆蓋equals方法的建議:
- 使用==操作符檢查 ”參數(shù)是否為這個(gè)對(duì)象的引用“。
- 使用instanceof操作符檢查 ”參數(shù)是否為正確的類型“闰歪。
- 把參數(shù)轉(zhuǎn)換為正確的類型嚎研。
- 對(duì)于該類中的”關(guān)鍵域“,檢查參數(shù)中的域是否與對(duì)象中對(duì)應(yīng)的域相匹配库倘。
- 當(dāng)完成equals方法后临扮,要檢查是否滿足四個(gè)特性,最好測(cè)試檢查教翩。
- 覆蓋equals時(shí)總要覆蓋hashCode
- 不要企圖讓equals方法過(guò)于智能
- 不要將equals聲明中的Object對(duì)象替換為其他的類型杆勇。