您好,登錄后才能下訂單哦!
摘要
本文描述重載equals方法的技術(shù),這種技術(shù)即使是具現(xiàn)類的子類增加了字段也能保證equal語(yǔ)義的正確性。
在《Effective Java》的第8項(xiàng)中,Josh Bloch描述了當(dāng)繼承類作為面向?qū)ο笳Z(yǔ)言中的等價(jià)關(guān)系的基礎(chǔ)問(wèn)題,要保證派生類的equal正確性語(yǔ)義所會(huì)面對(duì)的困難。Bloch這樣寫到:
除非你忘記了面向?qū)ο蟪橄蟮暮锰?,否則在當(dāng)你繼承一個(gè)新類或在類中增加了一個(gè)值組件時(shí)你無(wú)法同時(shí)保證equal的語(yǔ)義依然正確
在《Programming in Scala》中的第28章演示了一種方法,這種方法允許即使繼承了新類,增加了新的值組件,equal的語(yǔ)義仍然能得到保證。雖然在這本書中這項(xiàng)技術(shù)是在使用Scala類環(huán)境中,但是這項(xiàng)技術(shù)同樣可以應(yīng)用于Java定義的類中。在本文中的描述來(lái)自于Programming in Scala中的文字描述,但是代碼被我從scala翻譯成了Java
常見(jiàn)的等價(jià)方法陷阱
java.lang.Object 類定義了equals這個(gè)方法,它的子類可以通過(guò)重載來(lái)覆蓋它。不幸的是,在面向?qū)ο笾袑懗稣_的equals方法是非常困難的。事實(shí)上,在研究了大量的Java代碼后,2007 paper的作者得出了如下的一個(gè)結(jié)論:
幾乎所有的equals方法的實(shí)現(xiàn)都是錯(cuò)誤的!
這個(gè)問(wèn)題是因?yàn)榈葍r(jià)是和很多其他的事物相關(guān)聯(lián)。例如其中之一,一個(gè)的類型C的錯(cuò)誤等價(jià)方法可能意味著你無(wú)法將這個(gè)類型C的對(duì)象可信賴的放入到容器中。比如說(shuō),你有兩個(gè)元素elem1和elem2他們都是類型C的對(duì)象,并且他們是相等,即 elem1.equals(elm2) 返回ture。但是,只要這個(gè)equals方法是錯(cuò)誤的實(shí)現(xiàn),那么你就有可能會(huì)看見(jiàn)如下的一些行為:
Set hashSet<c> = new java.util.HashSet<c>(); hashSet.add(elem1); hashSet.contains(elem2); // returns false!</c></c>
當(dāng)equals重載時(shí),這里有4個(gè)會(huì)引發(fā)equals行為不一致的常見(jiàn)陷阱:
定義了錯(cuò)誤的equals方法簽名(signature) Defining equals with the wrong signature.
重載了equals的但沒(méi)有同時(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; } // ... }
看上去非常明顯,但是按照這種方式來(lái)定義equals就是錯(cuò)誤的。
// An utterly wrong definition of equals public boolean equals(Point other) { return (this.getX() == other.getX() && this.getY() == other.getY()); }
這個(gè)方法有什么問(wèn)題呢?初看起來(lái),它工作的非常完美:
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è)容器中問(wèn)題就出現(xiàn)了:
import java.util.HashSet; HashSet<point> coll = new HashSet<point>(); coll.add(p1); System.out.println(coll.contains(p2)); // prints false</point></point>
為什么coll中沒(méi)有包含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
到底是那里出了了問(wèn)題?事實(shí)上,之前所給出的equals版本并沒(méi)有覆蓋Object類的equals方法,因?yàn)樗念愋筒煌?。下面是Object的equals方法的定義
public boolean equals(Object other)
因?yàn)镻oint類中的equals方法使用的是以Point類而非Object類做為參數(shù),因此它并沒(méi)有覆蓋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è)方法并沒(méi)有被覆蓋,因此它仍然是實(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的但沒(méi)有同時(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 (有可能)</point></point>
事實(shí)上,這個(gè)個(gè)結(jié)果不是100%的false,你也可能有返回ture的經(jīng)歷。如果你得到的結(jié)果是true的話,那么你試試其他的坐標(biāo)值,最終你一定會(huì)得到一個(gè)在集合中不包含的結(jié)果。導(dǎo)致這個(gè)結(jié)果的原因是Point重載了equals卻沒(méi)有重載hashCode。
注意上面例子的的容器是一個(gè)HashSet,這就意味著容器中的元素根據(jù)他們的哈希碼被被放入到”哈希桶 hash buckets”中。contains方法首先根據(jù)哈希碼在哈希桶中查找,然后讓桶中的所有元素和所給的參數(shù)進(jìn)行比較?,F(xiàn)在,雖然最后一個(gè)Point類的版本重定義了equals方法,但是它并沒(méi)有同時(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)的問(wèn)題是,它的實(shí)現(xiàn)違背了作為Object類的定義的hashCode的語(yǔ)義。
如果兩個(gè)對(duì)象根據(jù)equals(Object)方法是相等的,那么在這兩個(gè)對(duì)象上調(diào)用hashCode方法應(yīng)該產(chǎn)生同樣的值
事實(shí)上,在Java中,hashCode和equals需要一起被重定義是眾所周知的。此外,hashCode只可以依賴于equals依賴的域來(lái)產(chǎn)生值。對(duì)于Point這個(gè)類來(lái)說(shuō),下面的的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à)性的問(wèn)題。然而,關(guān)于類的等價(jià)性仍然有其他的問(wèn)題點(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方法被增加到類中來(lái),并允許客戶改變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</point></point>
現(xiàn)在如果你改變p中的一個(gè)域,這個(gè)集合中還會(huì)包含point嗎,我們將拭目以待。
p.setX(p.getX() + 1); System.out.println(coll.contains(p)); // (有可能)打印 false
看起來(lái)非常的奇怪。p去那里去了?如果你通過(guò)集合的迭代器來(lái)檢查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</point>
結(jié)果是,集合中不包含p,但是p在集合的元素中!到底發(fā)生了什么!當(dāng)然,所有的這一切都是在x域的修改后才發(fā)生的,p最終的的hashCode是在集合coll錯(cuò)誤的哈希桶中。即,原始哈希桶不再有其新值對(duì)應(yīng)的哈希碼。換句話說(shuō),p已經(jīng)在集合coll的是視野范圍之外,雖然他仍然屬于coll的元素。
從這個(gè)例子所得到的教訓(xùn)是,當(dāng)equals和hashCode依賴于會(huì)變化的狀態(tài)時(shí),那么就會(huì)給用戶帶來(lái)問(wèn)題。如果這樣的對(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ì)繼承原來(lái)默認(rèn)的equals和hashCode的實(shí)現(xiàn),因此當(dāng)我們修改了x域后p依然會(huì)呆在其原來(lái)在容器中應(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)該包含改過(guò)的信息。
對(duì)于任何非空值x,x.equals(null)應(yīng)該總返回false.
Point類的equals定義已經(jīng)被開(kāi)發(fā)成了足夠滿足equals規(guī)范的定義。然而,當(dāng)考慮到繼承的時(shí)候,事情就開(kāi)始變得非常復(fù)雜起來(lái)。比如說(shuō)有一個(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ì)象的比較是沒(méi)有問(wèn)題的,但是如果使用ColoredPoint和Point混合進(jìn)行比較就要出現(xiàn)問(wè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ì)于集合來(lái)說(shuō)將導(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</point></point></point></point>
因此雖然p和cp是等價(jià)的,但是contains測(cè)試中一個(gè)返回成功,另外一個(gè)卻返回失敗。
你如何修改equals的定義,才能使得這個(gè)方法滿足對(duì)稱性?本質(zhì)上說(shuō)有兩種方法,你可以使得這種關(guān)系變得更一般化或更嚴(yán)格。更一般化的意思是這一對(duì)對(duì)象,a和b,被用于進(jìn)行對(duì)比,無(wú)論是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)在的問(wè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è)可以通過(guò)修改Point類和ColoredPoint類的equals方法來(lái)達(dá)到。你能增加額外的比較來(lái)檢查是否運(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)。通常這看起來(lái)非常合理,但是這里也存在著另外一種爭(zhēng)論——這樣的比較過(guò)于嚴(yán)格了。
考慮我們?nèi)缦逻@種稍微的迂回的方式來(lá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)是沒(méi)有理由的。
canEqual 方法
到此,我們看其來(lái)似乎是遇到阻礙了,存在著一種正常的方式不僅可以在不同類繼承層次上定義等價(jià)性,并且保證其等價(jià)的規(guī)范性嗎?事實(shí)上,的確存在這樣的一種方法,但是這就要求除了重定義equals和hashCode外還要另外的定義一個(gè)方法?;舅悸肪褪窃谥剌dequals(和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è)額外的需求,通過(guò)canEquals方法來(lái)決定另外一個(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í)例卻是可以比較的,同樣沒(méi)有重定義等價(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</point></point>
這些例子顯示了如果父類在equals的實(shí)現(xiàn)定義并調(diào)用了canEquals,那么開(kāi)發(fā)人員實(shí)現(xiàn)的子類就能決定這個(gè)子類是否可以和它父類的實(shí)例進(jìn)行比較。例如ColoredPoint,因?yàn)樗浴币粋€(gè)著色點(diǎn)永遠(yuǎn)不可以等于普通不帶顏色的點(diǎn)重載了” canEqual,所以他們就不能比較。但是因?yàn)閜Anon引用的匿名子類沒(méi)有重載canEqual,因此它的實(shí)例就可以和Point的實(shí)例進(jìn)行對(duì)比。
canEqual方法的一個(gè)潛在的爭(zhēng)論是它是否違背了Liskov替換準(zhǔn)則(LSP)。例如,通過(guò)比較運(yùn)行態(tài)的類來(lái)實(shí)現(xiàn)的比較技術(shù)(譯者注:canEqual的前一版本,使用.getClass()的那個(gè)版本),將導(dǎo)致不能定義出一個(gè)子類,這個(gè)子類的實(shí)例可以和其父類進(jìn)行比較,因此就違背了LSP。這是因?yàn)?,LSP原則是這樣的,在任何你能使用父類的地方你都可以使用子類去替換它。在之前例子中,雖然cp的x,y坐標(biāo)匹配那些在集合中的點(diǎn),然而”coll.contains(cp)”仍然返回false,這看起來(lái)似乎違背得了LSP準(zhǔn)則,因?yàn)槟悴荒苓@里能使用Point的地方使用一個(gè)ColoredPointed。但是我們認(rèn)為這種解釋是錯(cuò)誤的,因?yàn)長(zhǎng)SP原則并沒(méi)有要求子類和父類的行為一致,而僅要求其行為能一種方式滿足父類的規(guī)范。
通過(guò)比較運(yùn)行態(tài)的類來(lái)編寫equals方法(譯者注:canEqual的前一版本,使用.getClass()的那個(gè)版本)的問(wèn)題并不是違背LSP準(zhǔn)則的問(wèn)題,但是它也沒(méi)有為你指明一種創(chuàng)建派生類的實(shí)例能和父類實(shí)例進(jìn)行對(duì)比的的方法。例如,我們使用這種運(yùn)行態(tài)比較的技術(shù)在之前的”coll.contains(pAnon)”將會(huì)返回false,并且這并不是我們希望的。相反我們希望“coll.contains(cp)”返回false,因?yàn)橥ㄟ^(guò)在ColoredPoint中重載的equals,我基本上可以說(shuō),一個(gè)在坐標(biāo)1,2上著色點(diǎn)和一個(gè)坐標(biāo)1,2上的普通點(diǎn)并不是一回事。然而,在最后的例子中,我們能傳遞Point兩種不同的子類實(shí)例到集合中contains方法,并且我們能得到兩個(gè)不同的答案,并且這兩個(gè)答案都正確。
總結(jié)
以上就是本文關(guān)于Java編程中避免equals方法的隱藏陷阱介紹的全部?jī)?nèi)容,希望對(duì)大家有所幫助。感興趣的朋友可以繼續(xù)參閱本站:
Java中的hashcode方法介紹
創(chuàng)建并運(yùn)行一個(gè)java線程方法介紹
淺談java中==以及equals方法的用法
如有不足之處,歡迎留言指出。感謝朋友們對(duì)本站的支持!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。