Java編程中避免equals方法的隱藏陷阱介紹
摘要
本文描述重載equals方法的技術(shù),這種技術(shù)即使是具現(xiàn)類的子類增加了字段也能保證equal語義的正確性。
在《Effective Java》的第8項(xiàng)中,Josh Bloch描述了當(dāng)繼承類作為面向?qū)ο笳Z言中的等價(jià)關(guān)系的基礎(chǔ)問題,要保證派生類的equal正確性語義所會面對的困難。Bloch這樣寫到:
除非你忘記了面向?qū)ο蟪橄蟮暮锰?,否則在當(dāng)你繼承一個新類或在類中增加了一個值組件時你無法同時保證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這個方法,它的子類可以通過重載來覆蓋它。不幸的是,在面向?qū)ο笾袑懗稣_的equals方法是非常困難的。事實(shí)上,在研究了大量的Java代碼后,2007 paper的作者得出了如下的一個結(jié)論:
幾乎所有的equals方法的實(shí)現(xiàn)都是錯誤的!
這個問題是因?yàn)榈葍r(jià)是和很多其他的事物相關(guān)聯(lián)。例如其中之一,一個的類型C的錯誤等價(jià)方法可能意味著你無法將這個類型C的對象可信賴的放入到容器中。比如說,你有兩個元素elem1和elem2他們都是類型C的對象,并且他們是相等,即 elem1.equals(elm2) 返回ture。但是,只要這個equals方法是錯誤的實(shí)現(xiàn),那么你就有可能會看見如下的一些行為:
Set hashSet<c> = new java.util.HashSet<c>(); hashSet.add(elem1); hashSet.contains(elem2); // returns false!</c></c>
當(dāng)equals重載時,這里有4個會引發(fā)equals行為不一致的常見陷阱:
定義了錯誤的equals方法簽名(signature) Defining equals with the wrong signature.
重載了equals的但沒有同時重載hashCode的方法。 Changing equals without also changing hashCode.
建立在會變化字域上的equals定義。 Defining equals in terms of mutable fields.
不滿足等價(jià)關(guān)系的equals錯誤定義 Failing to define equals as an equivalence relation.
在剩下的章節(jié)中我們將依次討論這4中陷阱。
陷阱1:定義錯誤equals方法簽名(signature)
考慮為下面這個簡單類Point增加一個等價(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就是錯誤的。
// An utterly wrong definition of equals public boolean equals(Point other) { return (this.getX() == other.getX() && this.getY() == other.getY()); }
這個方法有什么問題呢?初看起來,它工作的非常完美:
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)我們一旦把這個Point類的實(shí)例放入到一個容器中問題就出現(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中沒有包含p2呢?甚至是p1也被加到集合里面,p1和p2是是等價(jià)的對象嗎?在下面的程序中,我們可以找到其中的一些原因,定義p2a是一個指向p2的對象,但是p2a的類型是Object而非Point類型:
Object p2a = p2;
現(xiàn)在我們重復(fù)第一個比較,但是不再使用p2而是p2a,我們將會得到如下的結(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時,Object類的equals就被調(diào)用。因?yàn)檫@個方法并沒有被覆蓋,因此它仍然是實(shí)現(xiàn)成比較對象標(biāo)示。這就是為什么雖然p1和p2a具有同樣的x,y值,”p1.equals(p2a)”仍然返回了false。這也是會什么HasSet的contains方法返回false的原因,因?yàn)檫@個方法操作的是泛型,他調(diào)用的是一般化的Object上equals方法而非Point類上變化了的重載方法equals
一個更好但不完美的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有了正確的類型,它使用了一個Object類型的參數(shù)和一個返回布爾型的結(jié)果。這個方法的實(shí)現(xiàn)使用instanceof操作和做了一個造型。它首先檢查這個對象是否是一個Point類,如果是,他就比較兩個點(diǎn)的坐標(biāo)并返回結(jié)果,否則返回false。
陷阱2:重載了equals的但沒有同時重載hashCode的方法
如果你使用上一個定義的Point類進(jìn)行p1和p2a的反復(fù)比較,你都會得到你預(yù)期的true的結(jié)果。但是如果你將這個類對象放入到HashSet.contains()方法中測試,你就有可能仍然得到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í)上,這個個結(jié)果不是100%的false,你也可能有返回ture的經(jīng)歷。如果你得到的結(jié)果是true的話,那么你試試其他的坐標(biāo)值,最終你一定會得到一個在集合中不包含的結(jié)果。導(dǎo)致這個結(jié)果的原因是Point重載了equals卻沒有重載hashCode。
注意上面例子的的容器是一個HashSet,這就意味著容器中的元素根據(jù)他們的哈希碼被被放入到”哈希桶 hash buckets”中。contains方法首先根據(jù)哈希碼在哈希桶中查找,然后讓桶中的所有元素和所給的參數(shù)進(jìn)行比較?,F(xiàn)在,雖然最后一個Point類的版本重定義了equals方法,但是它并沒有同時重定義hashCode。因此,hashCode仍然是Object類的那個版本,即:所分配對象的一個地址的變換。所以p1和p2的哈希碼理所當(dāng)然的不同了,甚至是即時這兩個點(diǎn)的坐標(biāo)完全相同。不同的哈希碼導(dǎo)致他們具有極高的可能性被放入到集合中不同的哈希桶中。contains方法將會去找p2的哈希碼對應(yīng)哈希桶中的匹配元素。但是大多數(shù)情況下,p1一定是在另外一個桶中,因此,p2永遠(yuǎn)找不到p1進(jìn)行匹配。當(dāng)然p2和p2也可能偶爾會被放入到一個桶中,在這種情況下,contains的結(jié)果就為true了。
最新一個Point類實(shí)現(xiàn)的問題是,它的實(shí)現(xiàn)違背了作為Object類的定義的hashCode的語義。
如果兩個對象根據(jù)equals(Object)方法是相等的,那么在這兩個對象上調(diào)用hashCode方法應(yīng)該產(chǎn)生同樣的值
事實(shí)上,在Java中,hashCode和equals需要一起被重定義是眾所周知的。此外,hashCode只可以依賴于equals依賴的域來產(chǎn)生值。對于Point這個類來說,下面的的hashCode定義是一個非常合適的定義。
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一個可能的實(shí)現(xiàn)。x域加上常量41后的結(jié)果再乘與41并將結(jié)果在加上y域的值。這樣做就可以以低成本的運(yùn)行時間和低成本代碼大小得到一個哈希碼的合理的分布(譯者注:性價(jià)比相對較高的做法)。
增加hashCode方法重載修正了定義類似Point類等價(jià)性的問題。然而,關(guān)于類的等價(jià)性仍然有其他的問題點(diǎn)待發(fā)現(xiàn)。
陷阱3:建立在會變化字段上的equals定義
讓我們在Point類做一個非常微小的變化
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,并且兩個set方法被增加到類中來,并允許客戶改變x和y的值。equals和hashCode這個方法的定義現(xiàn)在是基于在這兩個會發(fā)生變化的域上,因此當(dāng)他們的域的值改變時,結(jié)果也就跟著改變。因此一旦你將這個point對象放入到集合中你將會看到非常神奇的效果。
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中的一個域,這個集合中還會包含point嗎,我們將拭目以待。
p.setX(p.getX() + 1); System.out.println(coll.contains(p)); // (有可能)打印 false
看起來非常的奇怪。p去那里去了?如果你通過集合的迭代器來檢查p是否包含,你將會得到更奇怪的結(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錯誤的哈希桶中。即,原始哈希桶不再有其新值對應(yīng)的哈希碼。換句話說,p已經(jīng)在集合coll的是視野范圍之外,雖然他仍然屬于coll的元素。
從這個例子所得到的教訓(xùn)是,當(dāng)equals和hashCode依賴于會變化的狀態(tài)時,那么就會給用戶帶來問題。如果這樣的對象被放入到集合中,用戶必須小心,不要修改這些這些對象所依賴的狀態(tài),這是一個小陷阱。如果你需要根據(jù)對象當(dāng)前的狀態(tài)進(jìn)行比較的話,你應(yīng)該不要再重定義equals,應(yīng)該起其他的方法名字而不是equals。對于我們的Point類的最后的定義,我們最好省略掉hashCode的重載,并將比較的方法名命名為equalsContents,或其他不同于equals的名字。那么Point將會繼承原來默認(rèn)的equals和hashCode的實(shí)現(xiàn),因此當(dāng)我們修改了x域后p依然會呆在其原來在容器中應(yīng)該在位置。
陷阱4:不滿足等價(jià)關(guān)系的equals錯誤定義
Object中的equals的規(guī)范闡述了equals方法必須實(shí)現(xiàn)在非null對象上的等價(jià)關(guān)系:
自反原則:對于任何非null值X,表達(dá)式x.equals(x)總返回true。
等價(jià)性:對于任何非空值x和y,那么當(dāng)且僅當(dāng)y.equals(x)返回真時,x.equals(y)返回真。
傳遞性:對于任何非空值x,y,和z,如果x.equals(y)返回真,且y.equals(z)也返回真,那么x.equals(z)也應(yīng)該返回真。
一致性:對于非空x,y,多次調(diào)用x.equals(y)應(yīng)該一致的返回真或假。提供給equals方法比較使用的信息不應(yīng)該包含改過的信息。
對于任何非空值x,x.equals(null)應(yīng)該總返回false.
Point類的equals定義已經(jīng)被開發(fā)成了足夠滿足equals規(guī)范的定義。然而,當(dāng)考慮到繼承的時候,事情就開始變得非常復(fù)雜起來。比如說有一個Point的子類ColoredPoint,它比Point多增加了一個類型是Color的color域。假設(shè)Color被定義為一個枚舉類型:
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ī)范仍然是有效,如果兩個著色點(diǎn)(colored point)相等,其坐標(biāo)必定相等,因此它的hashCode也保證了具有同樣的值。
對于ColoredPoint類自身對象的比較是沒有問題的,但是如果使用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”的比較這個調(diào)用的是定義在Point類上的equals方法。這個方法只考慮兩個點(diǎn)的坐標(biāo)。因此比較返回真。在另外一方面,“cp等價(jià)于p”的比較這個調(diào)用的是定義在ColoredPoint類上的equals方法,返回的結(jié)果卻是false,這是因?yàn)閜不是ColoredPoint,所以equals這個定義違背了對稱性。
違背對稱性對于集合來說將導(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測試中一個返回成功,另外一個卻返回失敗。
你如何修改equals的定義,才能使得這個方法滿足對稱性?本質(zhì)上說有兩種方法,你可以使得這種關(guān)系變得更一般化或更嚴(yán)格。更一般化的意思是這一對對象,a和b,被用于進(jìn)行對比,無論是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的新定義比老定義中檢查了更多的情況:如果對象是一個Point對象而不是ColoredPoint,方法就轉(zhuǎn)變?yōu)镻oint類的equals方法調(diào)用。這個所希望達(dá)到的效果就是equals的對稱性,不管”cp.equals(p)”還是”p.equals(cp)”的結(jié)果都是true。然而這種方法,equals的規(guī)范還是被破壞了,現(xiàn)在的問題是這個新等價(jià)性不滿足傳遞性。考慮下面的一段代碼實(shí)例,定義了一個點(diǎn)和這個點(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
然而,對比redP和blueP的結(jié)果是false:
System.out.println(redP.equals(blueP)); // 打印 false
因此,equals的傳遞性就被違背了。
使equals的關(guān)系更一般化似乎會將我們帶入到死胡同。我們應(yīng)該采用更嚴(yán)格化的方法。一種更嚴(yán)格化的equals方法是認(rèn)為不同類的對象是不同的。這個可以通過修改Point類和ColoredPoint類的equals方法來達(dá)到。你能增加額外的比較來檢查是否運(yùn)行態(tài)的這個Point類和那個Point類是同一個類,就像如下所示的代碼一樣:
// 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)用回剛才那個不滿足對稱性要的equals實(shí)現(xiàn)了。
public class ColoredPoint extends Point { // 不再違反對稱性需求 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)和另外一個對象是同樣類,并且有同樣的坐標(biāo)時候,他們才被認(rèn)為是相等的,即意味著 .getClass()返回的是同樣的值。這個新定義的等價(jià)關(guān)系滿足了對稱性和傳遞性因?yàn)閷τ诒容^對象是不同的類時結(jié)果總是false。所以著色點(diǎn)(colored point)永遠(yuǎn)不會等于點(diǎn)(point)。通常這看起來非常合理,但是這里也存在著另外一種爭論——這樣的比較過于嚴(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對象不同。p是Point,而pAnon是Point的一個匿名派生類。但是,非常清晰的是pAnon的確是在坐標(biāo)1,2上的另外一個點(diǎn)。所以將他們認(rèn)為是不同的點(diǎn)是沒有理由的。
canEqual 方法
到此,我們看其來似乎是遇到阻礙了,存在著一種正常的方式不僅可以在不同類繼承層次上定義等價(jià)性,并且保證其等價(jià)的規(guī)范性嗎?事實(shí)上,的確存在這樣的一種方法,但是這就要求除了重定義equals和hashCode外還要另外的定義一個方法?;舅悸肪褪窃谥剌dequals(和hashCode)的同時,它應(yīng)該也要要明確的聲明這個類的對象永遠(yuǎn)不等價(jià)于其他的實(shí)現(xiàn)了不同等價(jià)方法的超類的對象。為了達(dá)到這個目標(biāo),我們對每一個重載了equals的類新增一個方法canEqual方法。這個方法的方法簽名是:
public boolean canEqual(Object other)
如果other 對象是canEquals(重)定義那個類的實(shí)例時,那么這個方法應(yīng)該返回真,否則返回false。這個方法由equals方法調(diào)用,并保證了兩個對象是可以相互比較的。下面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); } }
這個版本的Point類的equals方法中包含了一個額外的需求,通過canEquals方法來決定另外一個對象是否是是滿足可以比較的對象。在Point中的canEqual宣稱了所有的Point類實(shí)例都能被比較。
下面是ColoredPoint相應(yīng)的實(shí)現(xiàn)
public class ColoredPoint extends Point { // 不再違背對稱性 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à)是對稱和可傳遞的。比較一個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不是一個ColoredPoint,所以在ColoredPoint的equals方法體內(nèi)的第一個instanceof檢查就失敗了。
另外一個方面,不同的Point子類的實(shí)例卻是可以比較的,同樣沒有重定義等價(jià)性方法的類也是可以比較的。對于這個新類的定義,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,那么開發(fā)人員實(shí)現(xiàn)的子類就能決定這個子類是否可以和它父類的實(shí)例進(jìn)行比較。例如ColoredPoint,因?yàn)樗浴币粋€著色點(diǎn)永遠(yuǎn)不可以等于普通不帶顏色的點(diǎn)重載了” canEqual,所以他們就不能比較。但是因?yàn)閜Anon引用的匿名子類沒有重載canEqual,因此它的實(shí)例就可以和Point的實(shí)例進(jìn)行對比。
canEqual方法的一個潛在的爭論是它是否違背了Liskov替換準(zhǔn)則(LSP)。例如,通過比較運(yùn)行態(tài)的類來實(shí)現(xiàn)的比較技術(shù)(譯者注:canEqual的前一版本,使用.getClass()的那個版本),將導(dǎo)致不能定義出一個子類,這個子類的實(shí)例可以和其父類進(jìn)行比較,因此就違背了LSP。這是因?yàn)?,LSP原則是這樣的,在任何你能使用父類的地方你都可以使用子類去替換它。在之前例子中,雖然cp的x,y坐標(biāo)匹配那些在集合中的點(diǎn),然而”coll.contains(cp)”仍然返回false,這看起來似乎違背得了LSP準(zhǔn)則,因?yàn)槟悴荒苓@里能使用Point的地方使用一個ColoredPointed。但是我們認(rèn)為這種解釋是錯誤的,因?yàn)長SP原則并沒有要求子類和父類的行為一致,而僅要求其行為能一種方式滿足父類的規(guī)范。
通過比較運(yùn)行態(tài)的類來編寫equals方法(譯者注:canEqual的前一版本,使用.getClass()的那個版本)的問題并不是違背LSP準(zhǔn)則的問題,但是它也沒有為你指明一種創(chuàng)建派生類的實(shí)例能和父類實(shí)例進(jìn)行對比的的方法。例如,我們使用這種運(yùn)行態(tài)比較的技術(shù)在之前的”coll.contains(pAnon)”將會返回false,并且這并不是我們希望的。相反我們希望“coll.contains(cp)”返回false,因?yàn)橥ㄟ^在ColoredPoint中重載的equals,我基本上可以說,一個在坐標(biāo)1,2上著色點(diǎn)和一個坐標(biāo)1,2上的普通點(diǎn)并不是一回事。然而,在最后的例子中,我們能傳遞Point兩種不同的子類實(shí)例到集合中contains方法,并且我們能得到兩個不同的答案,并且這兩個答案都正確。
總結(jié)
以上就是本文關(guān)于Java編程中避免equals方法的隱藏陷阱介紹的全部內(nèi)容,希望對大家有所幫助。感興趣的朋友可以繼續(xù)參閱本站:
創(chuàng)建并運(yùn)行一個java線程方法介紹
如有不足之處,歡迎留言指出。感謝朋友們對本站的支持!
相關(guān)文章
java連接mysql數(shù)據(jù)庫及測試是否連接成功的方法
這篇文章主要介紹了java連接mysql數(shù)據(jù)庫及測試是否連接成功的方法,結(jié)合完整實(shí)例形式分析了java基于jdbc連接mysql數(shù)據(jù)庫并返回連接狀態(tài)的具體步驟與相關(guān)操作技巧,需要的朋友可以參考下2017-09-09面試必問項(xiàng)之Set實(shí)現(xiàn)類:TreeSet
這篇文章主要介紹了Java TreeSet類的簡單理解和使用,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2021-07-07Java Map 在put值時value值不被覆蓋的解決辦法
這篇文章主要介紹了Java Map 在put值時value值不被覆蓋的解決辦法,非常不錯,具有參考借鑒價(jià)值,需要的朋友可以參考下2017-04-04實(shí)例分析Java Class的文件結(jié)構(gòu)
今天把之前在Evernote中的筆記重新整理了一下,發(fā)上來供對java class 文件結(jié)構(gòu)的有興趣的同學(xué)參考一下2013-04-04SpringBoot開發(fā)技巧之使用AOP記錄日志示例解析
這篇文章主要為大家介紹了SpringBoot開發(fā)技巧之如何利用AOP記錄日志的示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2021-10-10Spring使用ThreadPoolTaskExecutor自定義線程池及異步調(diào)用方式
這篇文章主要介紹了Spring使用ThreadPoolTaskExecutor自定義線程池及異步調(diào)用方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02idea2019.2安裝MybatisCodeHelper插件的超詳細(xì)教程
這篇文章主要介紹了idea2019.2安裝MybatisCodeHelper插件的教程,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09