如何在Java中避免equals方法的隱藏陷阱
譯者:趙錕
zhaokun.km (at)gmail.com
轉(zhuǎn)自:http://cocre.com/?p=1051 酷殼
譯者注 :你可能會(huì)覺得Java很簡(jiǎn)單,Object的equals實(shí)現(xiàn)也會(huì)非常簡(jiǎn)單,但是事實(shí)并不是你想象的這樣,耐心的讀完本文,你會(huì)發(fā)現(xiàn)你對(duì)Java了解的是如此的少。如果這篇文章是一份Java程序員的入職筆試,那么不知道有多少人會(huì)掉落到這樣的陷阱中。原文轉(zhuǎn)自http://www.artima.com/lejava/articles/equality.html 三位作者都是不同領(lǐng)域的大拿,有興趣的讀者可以從上面這個(gè)連接直接去閱讀原文。
摘要
本文描述重載equals方法的技術(shù),這種技術(shù)即使是具現(xiàn)類的子類增加了字段也能保證equal語義的正確性。
在《Effective Java》的第8項(xiàng)中,Josh Bloch描述了當(dāng)繼承類作為面向?qū)ο笳Z言中的等價(jià)關(guān)系的基礎(chǔ)問題,要保證派生類的equal正確性語義所會(huì)面對(duì)的困難。Bloch這樣寫到:
除非你忘記了面向?qū)ο蟪橄蟮暮锰帲駝t在當(dāng)你繼承一個(gè)新類或在類中增加了一個(gè)值組件時(shí)你無法同時(shí)保證equal的語義依然正確
在《Programming in Scala》中的第28章演示了一種方法,這種方法允許即使繼承了新類,增加了新的值組件,equal的語義仍然能得到保證。雖然在這本書中這項(xiàng)技術(shù)是在使用Scala類環(huán)境中,但是這項(xiàng)技術(shù)同樣可以應(yīng)用于Java定義的類中。在本文中的描述來自于Programming in Scala中的文字描述,但是代碼被我從scala翻譯成了Java
常見的等價(jià)方法陷阱
java.lang.Object 類定義了equals這個(gè)方法,它的子類可以通過重載來覆蓋它。不幸的是,在面向?qū)ο笾袑懗稣_的equals方法是非常困難的。事實(shí)上,在研究了大量的Java代碼后,2007 paper的作者得出了如下的一個(gè)結(jié)論:
幾乎所有的equals方法的實(shí)現(xiàn)都是錯(cuò)誤的!
這個(gè)問題是因?yàn)榈葍r(jià)是和很多其他的事物相關(guān)聯(lián)。例如其中之一,一個(gè)的類型C的錯(cuò)誤等價(jià)方法可能意味著你無法將這個(gè)類型C的對(duì)象可信賴的放入到容器中。比如說,你有兩個(gè)元素elem1和elem2他們都是類型C的對(duì)象,并且他們是相等,即elem1.equals(elm2)返回ture。但是,只要這個(gè)equals方法是錯(cuò)誤的實(shí)現(xiàn),那么你就有可能會(huì)看見如下的一些行為:
Set hashSet<C> = new java.util.HashSet<C>(); hashSet.add(elem1); hashSet.contains(elem2); // returns false!
當(dāng)equals重載時(shí),這里有4個(gè)會(huì)引發(fā)equals行為不一致的常見陷阱:
- 定義了錯(cuò)誤的equals方法簽名(signature) Defining equals with the wrong signature.
- 重載了equals的但沒有同時(shí)重載hashCode的方法。 Changing equals without also changing hashCode.
- 建立在會(huì)變化字域上的equals定義。 Defining equals in terms of mutable fields.
- 不滿足等價(jià)關(guān)系的equals錯(cuò)誤定義 Failing to define equals as an equivalence relation.
在剩下的章節(jié)中我們將依次討論這4中陷阱。
陷阱1:定義錯(cuò)誤equals方法簽名(signature)
考慮為下面這個(gè)簡(jiǎn)單類Point增加一個(gè)等價(jià)性方法:
public class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } // ... }
看上去非常明顯,但是按照這種方式來定義equals就是錯(cuò)誤的。
// An utterly wrong definition of equals public boolean equals(Point other) { return (this.getX() == other.getX() && this.getY() == other.getY()); }
這個(gè)方法有什么問題呢?初看起來,它工作的非常完美:
Point p1 = new Point(1, 2); Point p2 = new Point(1, 2); Point q = new Point(2, 3); System.out.println(p1.equals(p2)); // prints true System.out.println(p1.equals(q)); // prints false
然而,當(dāng)我們一旦把這個(gè)Point類的實(shí)例放入到一個(gè)容器中問題就出現(xiàn)了:
import java.util.HashSet; HashSet<Point> coll = new HashSet<Point>(); coll.add(p1); System.out.println(coll.contains(p2)); // prints false
為什么coll中沒有包含p2呢?甚至是p1也被加到集合里面,p1和p2是是等價(jià)的對(duì)象嗎?在下面的程序中,我們可以找到其中的一些原因,定義p2a是一個(gè)指向p2的對(duì)象,但是p2a的類型是Object而非Point類型:
Object p2a = p2;
現(xiàn)在我們重復(fù)第一個(gè)比較,但是不再使用p2而是p2a,我們將會(huì)得到如下的結(jié)果:
System.out.println(p1.equals(p2a)); // prints false
到底是那里出了了問題?事實(shí)上,之前所給出的equals版本并沒有覆蓋Object類的equals方法,因?yàn)樗念愋筒煌O旅媸荗bject的equals方法的定義
public boolean equals(Object other)
因?yàn)镻oint類中的equals方法使用的是以Point類而非Object類做為參數(shù),因此它并沒有覆蓋Object中的equals方法。而是一種變化了的重載。在Java中重載被解析為靜態(tài)的參數(shù)類型而非運(yùn)行期的類型,因此當(dāng)靜態(tài)參數(shù)類型是Point,Point的equals方法就被調(diào)用。然而當(dāng)靜態(tài)參數(shù)類型是Object時(shí),Object類的equals就被調(diào)用。因?yàn)檫@個(gè)方法并沒有被覆蓋,因此它仍然是實(shí)現(xiàn)成比較對(duì)象標(biāo)示。這就是為什么雖然p1和p2a具有同樣的x,y值,”p1.equals(p2a)”仍然返回了false。這也是會(huì)什么HasSet的contains方法返回false的原因,因?yàn)檫@個(gè)方法操作的是泛型,他調(diào)用的是一般化的Object上equals方法而非Point類上變化了的重載方法equals
一個(gè)更好但不完美的equals方法定義如下:
// A better definition, but still not perfect @Override public boolean equals(Object other) { boolean result = false; if (other instanceof Point) { Point that = (Point) other; result = (this.getX() == that.getX() && this.getY() == that.getY()); } return result; }
現(xiàn)在equals有了正確的類型,它使用了一個(gè)Object類型的參數(shù)和一個(gè)返回布爾型的結(jié)果。這個(gè)方法的實(shí)現(xiàn)使用instanceof操作和做了一個(gè)造型。它首先檢查這個(gè)對(duì)象是否是一個(gè)Point類,如果是,他就比較兩個(gè)點(diǎn)的坐標(biāo)并返回結(jié)果,否則返回false。
陷阱2:重載了equals的但沒有同時(shí)重載hashCode的方法
如果你使用上一個(gè)定義的Point類進(jìn)行p1和p2a的反復(fù)比較,你都會(huì)得到你預(yù)期的true的結(jié)果。但是如果你將這個(gè)類對(duì)象放入到HashSet.contains()方法中測(cè)試,你就有可能仍然得到false的結(jié)果:
Point p1 = new Point(1, 2); Point p2 = new Point(1, 2); HashSet<Point> coll = new HashSet<Point>(); coll.add(p1); System.out.println(coll.contains(p2)); // 打印 false (有可能)
事實(shí)上,這個(gè)個(gè)結(jié)果不是100%的false,你也可能有返回ture的經(jīng)歷。如果你得到的結(jié)果是true的話,那么你試試其他的坐標(biāo)值,最終你一定會(huì)得到一個(gè)在集合中不包含的結(jié)果。導(dǎo)致這個(gè)結(jié)果的原因是Point重載了equals卻沒有重載hashCode。
注意上面例子的的容器是一個(gè)HashSet,這就意味著容器中的元素根據(jù)他們的哈希碼被被放入到”哈希桶 hash buckets”中。contains方法首先根據(jù)哈希碼在哈希桶中查找,然后讓桶中的所有元素和所給的參數(shù)進(jìn)行比較。現(xiàn)在,雖然最后一個(gè)Point類的版本重定義了equals方法,但是它并沒有同時(shí)重定義hashCode。因此,hashCode仍然是Object類的那個(gè)版本,即:所分配對(duì)象的一個(gè)地址的變換。所以p1和p2的哈希碼理所當(dāng)然的不同了,甚至是即時(shí)這兩個(gè)點(diǎn)的坐標(biāo)完全相同。不同的哈希碼導(dǎo)致他們具有極高的可能性被放入到集合中不同的哈希桶中。contains方法將會(huì)去找p2的哈希碼對(duì)應(yīng)哈希桶中的匹配元素。但是大多數(shù)情況下,p1一定是在另外一個(gè)桶中,因此,p2永遠(yuǎn)找不到p1進(jìn)行匹配。當(dāng)然p2和p2也可能偶爾會(huì)被放入到一個(gè)桶中,在這種情況下,contains的結(jié)果就為true了。
最新一個(gè)Point類實(shí)現(xiàn)的問題是,它的實(shí)現(xiàn)違背了作為Object類的定義的hashCode的語義。
如果兩個(gè)對(duì)象根據(jù)equals(Object)方法是相等的,那么在這兩個(gè)對(duì)象上調(diào)用hashCode方法應(yīng)該產(chǎn)生同樣的值
事實(shí)上,在Java中,hashCode和equals需要一起被重定義是眾所周知的。此外,hashCode只可以依賴于equals依賴的域來產(chǎn)生值。對(duì)于Point這個(gè)類來說,下面的的hashCode定義是一個(gè)非常合適的定義。
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof Point) {
Point that = (Point) other;
result = (this.getX() == that.getX() && this.getY() == that.getY());
}
return result;
}
@Override public int hashCode() {
return (41 * (41 + getX()) + getY());
}
}
這只是hashCode一個(gè)可能的實(shí)現(xiàn)。x域加上常量41后的結(jié)果再乘與41并將結(jié)果在加上y域的值。這樣做就可以以低成本的運(yùn)行時(shí)間和低成本代碼大小得到一個(gè)哈希碼的合理的分布(譯者注:性價(jià)比相對(duì)較高的做法)。
增加hashCode方法重載修正了定義類似Point類等價(jià)性的問題。然而,關(guān)于類的等價(jià)性仍然有其他的問題點(diǎn)待發(fā)現(xiàn)。
陷阱3:建立在會(huì)變化字段上的equals定義
讓我們?cè)赑oint類做一個(gè)非常微小的變化
public class Point { private int x; private int y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } public void setX(int x) { // Problematic this.x = x; } public void setY(int y) { this.y = y; } @Override public boolean equals(Object other) { boolean result = false; if (other instanceof Point) { Point that = (Point) other; result = (this.getX() == that.getX() && this.getY() == that.getY()); } return result; } @Override public int hashCode() { return (41 * (41 + getX()) + getY()); } }
唯一的不同是x和y域不再是final,并且兩個(gè)set方法被增加到類中來,并允許客戶改變x和y的值。equals和hashCode這個(gè)方法的定義現(xiàn)在是基于在這兩個(gè)會(huì)發(fā)生變化的域上,因此當(dāng)他們的域的值改變時(shí),結(jié)果也就跟著改變。因此一旦你將這個(gè)point對(duì)象放入到集合中你將會(huì)看到非常神奇的效果。
Point p = new Point(1, 2); HashSet<Point> coll = new HashSet<Point>(); coll.add(p); System.out.println(coll.contains(p)); // 打印 true
現(xiàn)在如果你改變p中的一個(gè)域,這個(gè)集合中還會(huì)包含point嗎,我們將拭目以待。
p.setX(p.getX() + 1); System.out.println(coll.contains(p)); // (有可能)打印 false
看起來非常的奇怪。p去那里去了?如果你通過集合的迭代器來檢查p是否包含,你將會(huì)得到更奇怪的結(jié)果。
Iterator<Point> it = coll.iterator(); boolean containedP = false; while (it.hasNext()) { Point nextP = it.next(); if (nextP.equals(p)) { containedP = true; break; } } System.out.println(containedP); // 打印 true
結(jié)果是,集合中不包含p,但是p在集合的元素中!到底發(fā)生了什么!當(dāng)然,所有的這一切都是在x域的修改后才發(fā)生的,p最終的的hashCode是在集合coll錯(cuò)誤的哈希桶中。即,原始哈希桶不再有其新值對(duì)應(yīng)的哈希碼。換句話說,p已經(jīng)在集合coll的是視野范圍之外,雖然他仍然屬于coll的元素。
從這個(gè)例子所得到的教訓(xùn)是,當(dāng)equals和hashCode依賴于會(huì)變化的狀態(tài)時(shí),那么就會(huì)給用戶帶來問題。如果這樣的對(duì)象被放入到集合中,用戶必須小心,不要修改這些這些對(duì)象所依賴的狀態(tài),這是一個(gè)小陷阱。如果你需要根據(jù)對(duì)象當(dāng)前的狀態(tài)進(jìn)行比較的話,你應(yīng)該不要再重定義equals,應(yīng)該起其他的方法名字而不是equals。對(duì)于我們的Point類的最后的定義,我們最好省略掉hashCode的重載,并將比較的方法名命名為equalsContents,或其他不同于equals的名字。那么Point將會(huì)繼承原來默認(rèn)的equals和hashCode的實(shí)現(xiàn),因此當(dāng)我們修改了x域后p依然會(huì)呆在其原來在容器中應(yīng)該在位置。
陷阱4:不滿足等價(jià)關(guān)系的equals錯(cuò)誤定義
Object中的equals的規(guī)范闡述了equals方法必須實(shí)現(xiàn)在非null對(duì)象上的等價(jià)關(guān)系:
- 自反原則:對(duì)于任何非null值X,表達(dá)式x.equals(x)總返回true。
- 等價(jià)性:對(duì)于任何非空值x和y,那么當(dāng)且僅當(dāng)y.equals(x)返回真時(shí),x.equals(y)返回真。
- 傳遞性:對(duì)于任何非空值x,y,和z,如果x.equals(y)返回真,且y.equals(z)也返回真,那么x.equals(z)也應(yīng)該返回真。
- 一致性:對(duì)于非空x,y,多次調(diào)用x.equals(y)應(yīng)該一致的返回真或假。提供給equals方法比較使用的信息不應(yīng)該包含改過的信息。
- 對(duì)于任何非空值x,x.equals(null)應(yīng)該總返回false.
Point類的equals定義已經(jīng)被開發(fā)成了足夠滿足equals規(guī)范的定義。然而,當(dāng)考慮到繼承的時(shí)候,事情就開始變得非常復(fù)雜起來。比如說有一個(gè)Point的子類ColoredPoint,它比Point多增加了一個(gè)類型是Color的color域。假設(shè)Color被定義為一個(gè)枚舉類型:
public enum Color { RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET; }
ColoredPoint重載了equals方法,并考慮到新加入color域,代碼如下:
public class ColoredPoint extends Point { // Problem: equals not symmetric private final Color color; public ColoredPoint(int x, int y, Color color) { super(x, y); this.color = color; } @Override public boolean equals(Object other) { boolean result = false; if (other instanceof ColoredPoint) { ColoredPoint that = (ColoredPoint) other; result = (this.color.equals(that.color) && super.equals(that)); } return result; } }
這是很多程序員都有可能寫成的代碼。注意在本例中,類ColoredPointed不需要重載hashCode,因?yàn)樾碌腃oloredPoint類上的equals定義,嚴(yán)格的重載了Point上equals的定義。hashCode的規(guī)范仍然是有效,如果兩個(gè)著色點(diǎn)(colored point)相等,其坐標(biāo)必定相等,因此它的hashCode也保證了具有同樣的值。
對(duì)于ColoredPoint類自身對(duì)象的比較是沒有問題的,但是如果使用ColoredPoint和Point混合進(jìn)行比較就要出現(xiàn)問題。
Point p = new Point(1, 2); ColoredPoint cp = new ColoredPoint(1, 2, Color.RED); System.out.println(p.equals(cp)); // 打印真 true System.out.println(cp.equals(p)); // 打印假 false
“p等價(jià)于cp”的比較這個(gè)調(diào)用的是定義在Point類上的equals方法。這個(gè)方法只考慮兩個(gè)點(diǎn)的坐標(biāo)。因此比較返回真。在另外一方面,“cp等價(jià)于p”的比較這個(gè)調(diào)用的是定義在ColoredPoint類上的equals方法,返回的結(jié)果卻是false,這是因?yàn)閜不是ColoredPoint,所以equals這個(gè)定義違背了對(duì)稱性。
違背對(duì)稱性對(duì)于集合來說將導(dǎo)致不可以預(yù)期的后果,例如:
Set<Point> hashSet1 = new java.util.HashSet<Point>(); hashSet1.add(p); System.out.println(hashSet1.contains(cp)); // 打印 false Set<Point> hashSet2 = new java.util.HashSet<Point>(); hashSet2.add(cp); System.out.println(hashSet2.contains(p)); // 打印 true
因此雖然p和cp是等價(jià)的,但是contains測(cè)試中一個(gè)返回成功,另外一個(gè)卻返回失敗。
你如何修改equals的定義,才能使得這個(gè)方法滿足對(duì)稱性?本質(zhì)上說有兩種方法,你可以使得這種關(guān)系變得更一般化或更嚴(yán)格。更一般化的意思是這一對(duì)對(duì)象,a和b,被用于進(jìn)行對(duì)比,無論是a比b還是b比a 都返回true,下面是代碼:
public class ColoredPoint extends Point { // Problem: equals not transitive private final Color color; public ColoredPoint(int x, int y, Color color) { super(x, y); this.color = color; } @Override public boolean equals(Object other) { boolean result = false; if (other instanceof ColoredPoint) { ColoredPoint that = (ColoredPoint) other; result = (this.color.equals(that.color) && super.equals(that)); } else if (other instanceof Point) { Point that = (Point) other; result = that.equals(this); } return result; } }
在ColoredPoint中的equals的新定義比老定義中檢查了更多的情況:如果對(duì)象是一個(gè)Point對(duì)象而不是ColoredPoint,方法就轉(zhuǎn)變?yōu)镻oint類的equals方法調(diào)用。這個(gè)所希望達(dá)到的效果就是equals的對(duì)稱性,不管”cp.equals(p)”還是”p.equals(cp)”的結(jié)果都是true。然而這種方法,equals的規(guī)范還是被破壞了,現(xiàn)在的問題是這個(gè)新等價(jià)性不滿足傳遞性。考慮下面的一段代碼實(shí)例,定義了一個(gè)點(diǎn)和這個(gè)點(diǎn)上上兩種不同顏色點(diǎn):
ColoredPoint redP = new ColoredPoint(1, 2, Color.RED); ColoredPoint blueP = new ColoredPoint(1, 2, Color.BLUE);
redP等價(jià)于p,p等價(jià)于blueP
System.out.println(redP.equals(p)); // prints true System.out.println(p.equals(blueP)); // prints true
然而,對(duì)比redP和blueP的結(jié)果是false:
System.out.println(redP.equals(blueP)); // 打印 false
因此,equals的傳遞性就被違背了。
使equals的關(guān)系更一般化似乎會(huì)將我們帶入到死胡同。我們應(yīng)該采用更嚴(yán)格化的方法。一種更嚴(yán)格化的equals方法是認(rèn)為不同類的對(duì)象是不同的。這個(gè)可以通過修改Point類和ColoredPoint類的equals方法來達(dá)到。你能增加額外的比較來檢查是否運(yùn)行態(tài)的這個(gè)Point類和那個(gè)Point類是同一個(gè)類,就像如下所示的代碼一樣:
// A technically valid, but unsatisfying, equals method
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof Point) {
Point that = (Point) other;
result = (this.getX() == that.getX() && this.getY() == that.getY()
&& this.getClass().equals(that.getClass()));
}
return result;
}
@Override public int hashCode() {
return (41 * (41 + getX()) + getY());
}
}
你現(xiàn)在可以將ColoredPoint類的equals實(shí)現(xiàn)用回剛才那個(gè)不滿足對(duì)稱性要的equals實(shí)現(xiàn)了。
public class ColoredPoint extends Point { // 不再違反對(duì)稱性需求 private final Color color; public ColoredPoint(int x, int y, Color color) { super(x, y); this.color = color; } @Override public boolean equals(Object other) { boolean result = false; if (other instanceof ColoredPoint) { ColoredPoint that = (ColoredPoint) other; result = (this.color.equals(that.color) && super.equals(that)); } return result; } }
這里,Point類的實(shí)例只有當(dāng)和另外一個(gè)對(duì)象是同樣類,并且有同樣的坐標(biāo)時(shí)候,他們才被認(rèn)為是相等的,即意味著 .getClass()返回的是同樣的值。這個(gè)新定義的等價(jià)關(guān)系滿足了對(duì)稱性和傳遞性因?yàn)閷?duì)于比較對(duì)象是不同的類時(shí)結(jié)果總是false。所以著色點(diǎn)(colored point)永遠(yuǎn)不會(huì)等于點(diǎn)(point)。通常這看起來非常合理,但是這里也存在著另外一種爭(zhēng)論——這樣的比較過于嚴(yán)格了。
考慮我們?nèi)缦逻@種稍微的迂回的方式來定義我們的坐標(biāo)點(diǎn)(1,2)
Point pAnon = new Point(1, 1) { @Override public int getY() { return 2; } };
pAnon等于p嗎?答案是假,因?yàn)閜和pAnon的java.lang.Class對(duì)象不同。p是Point,而pAnon是Point的一個(gè)匿名派生類。但是,非常清晰的是pAnon的確是在坐標(biāo)1,2上的另外一個(gè)點(diǎn)。所以將他們認(rèn)為是不同的點(diǎn)是沒有理由的。
canEqual 方法
到此,我們看其來似乎是遇到阻礙了,存在著一種正常的方式不僅可以在不同類繼承層次上定義等價(jià)性,并且保證其等價(jià)的規(guī)范性嗎?事實(shí)上,的確存在這樣的一種方法,但是這就要求除了重定義equals和hashCode外還要另外的定義一個(gè)方法。基本思路就是在重載equals(和hashCode)的同時(shí),它應(yīng)該也要要明確的聲明這個(gè)類的對(duì)象永遠(yuǎn)不等價(jià)于其他的實(shí)現(xiàn)了不同等價(jià)方法的超類的對(duì)象。為了達(dá)到這個(gè)目標(biāo),我們對(duì)每一個(gè)重載了equals的類新增一個(gè)方法canEqual方法。這個(gè)方法的方法簽名是:
public boolean canEqual(Object other)
如果other 對(duì)象是canEquals(重)定義那個(gè)類的實(shí)例時(shí),那么這個(gè)方法應(yīng)該返回真,否則返回false。這個(gè)方法由equals方法調(diào)用,并保證了兩個(gè)對(duì)象是可以相互比較的。下面Point類的新的也是最終的實(shí)現(xiàn):
public class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } @Override public boolean equals(Object other) { boolean result = false; if (other instanceof Point) { Point that = (Point) other; result =(that.canEqual(this) && this.getX() == that.getX() && this.getY() == that.getY()); } return result; } @Override public int hashCode() { return (41 * (41 + getX()) + getY()); } public boolean canEqual(Object other) { return (other instanceof Point); } }
這個(gè)版本的Point類的equals方法中包含了一個(gè)額外的需求,通過canEquals方法來決定另外一個(gè)對(duì)象是否是是滿足可以比較的對(duì)象。在Point中的canEqual宣稱了所有的Point類實(shí)例都能被比較。
下面是ColoredPoint相應(yīng)的實(shí)現(xiàn)
public class ColoredPoint extends Point { // 不再違背對(duì)稱性 private final Color color; public ColoredPoint(int x, int y, Color color) { super(x, y); this.color = color; } @Override public boolean equals(Object other) { boolean result = false; if (other instanceof ColoredPoint) { ColoredPoint that = (ColoredPoint) other; result = (that.canEqual(this) && this.color.equals(that.color) && super.equals(that)); } return result; } @Override public int hashCode() { return (41 * super.hashCode() + color.hashCode()); } @Override public boolean canEqual(Object other) { return (other instanceof ColoredPoint); } }
在上顯示的新版本的Point類和ColoredPoint類定義保證了等價(jià)的規(guī)范。等價(jià)是對(duì)稱和可傳遞的。比較一個(gè)Point和ColoredPoint類總是返回false。因?yàn)辄c(diǎn)p和著色點(diǎn)cp,“p.equals(cp)返回的是假。并且,因?yàn)閏p.canEqual(p)總返回false。相反的比較,cp.equals(p)同樣也返回false,由于p不是一個(gè)ColoredPoint,所以在ColoredPoint的equals方法體內(nèi)的第一個(gè)instanceof檢查就失敗了。
另外一個(gè)方面,不同的Point子類的實(shí)例卻是可以比較的,同樣沒有重定義等價(jià)性方法的類也是可以比較的。對(duì)于這個(gè)新類的定義,p和pAnon的比較將總返回true。下面是一些例子:
Point p = new Point(1, 2); ColoredPoint cp = new ColoredPoint(1, 2, Color.INDIGO); Point pAnon = new Point(1, 1) { @Override public int getY() { return 2; } }; Set<Point> coll = new java.util.HashSet<Point>(); coll.add(p); System.out.println(coll.contains(p)); // 打印 true System.out.println(coll.contains(cp)); // 打印 false System.out.println(coll.contains(pAnon)); // 打印 true
這些例子顯示了如果父類在equals的實(shí)現(xiàn)定義并調(diào)用了canEquals,那么開發(fā)人員實(shí)現(xiàn)的子類就能決定這個(gè)子類是否可以和它父類的實(shí)例進(jìn)行比較。例如ColoredPoint,因?yàn)樗?#8221;一個(gè)著色點(diǎn)永遠(yuǎn)不可以等于普通不帶顏色的點(diǎn)重載了” canEqual,所以他們就不能比較。但是因?yàn)閜Anon引用的匿名子類沒有重載canEqual,因此它的實(shí)例就可以和Point的實(shí)例進(jìn)行對(duì)比。
canEqual方法的一個(gè)潛在的爭(zhēng)論是它是否違背了Liskov替換準(zhǔn)則(LSP)。例如,通過比較運(yùn)行態(tài)的類來實(shí)現(xiàn)的比較技術(shù)(譯者注: canEqual的前一版本,使用.getClass()的那個(gè)版本),將導(dǎo)致不能定義出一個(gè)子類,這個(gè)子類的實(shí)例可以和其父類進(jìn)行比較,因此就違背了LSP。這是因?yàn)椋琇SP原則是這樣的,在任何你能使用父類的地方你都可以使用子類去替換它。在之前例子中,雖然cp的x,y坐標(biāo)匹配那些在集合中的點(diǎn),然而”coll.contains(cp)”仍然返回false,這看起來似乎違背得了LSP準(zhǔn)則,因?yàn)槟悴荒苓@里能使用Point的地方使用一個(gè)ColoredPointed。但是我們認(rèn)為這種解釋是錯(cuò)誤的,因?yàn)長(zhǎng)SP原則并沒有要求子類和父類的行為一致,而僅要求其行為能一種方式滿足父類的規(guī)范。
通過比較運(yùn)行態(tài)的類來編寫equals方法(譯者注: canEqual的前一版本,使用.getClass()的那個(gè)版本)的問題并不是違背LSP準(zhǔn)則的問題,但是它也沒有為你指明一種創(chuàng)建派生類的實(shí)例能和父類實(shí)例進(jìn)行對(duì)比的的方法。例如,我們使用這種運(yùn)行態(tài)比較的技術(shù)在之前的”coll.contains(pAnon)”將會(huì)返回false,并且這并不是我們希望的。相反我們希望“coll.contains(cp)”返回false,因?yàn)橥ㄟ^在ColoredPoint中重載的equals,我基本上可以說,一個(gè)在坐標(biāo)1,2上著色點(diǎn)和一個(gè)坐標(biāo)1,2上的普通點(diǎn)并不是一回事。然而,在最后的例子中,我們能傳遞Point兩種不同的子類實(shí)例到集合中contains方法,并且我們能得到兩個(gè)不同的答案,并且這兩個(gè)答案都正確。
–全文完–