新聞中心
本文介紹了一種改寫(override)equals 方法的技巧。使用該技巧,即使在實(shí)體類的子類添加了新的域(field)時(shí),仍然能夠滿足 equals 方法的約定。

成都創(chuàng)新互聯(lián)公司是專業(yè)的金牛網(wǎng)站建設(shè)公司,金牛接單;提供成都做網(wǎng)站、成都網(wǎng)站制作,網(wǎng)頁(yè)設(shè)計(jì),網(wǎng)站設(shè)計(jì),建網(wǎng)站,PHP網(wǎng)站建設(shè)等專業(yè)做網(wǎng)站服務(wù);采用PHP框架,可快速的進(jìn)行金牛網(wǎng)站開發(fā)網(wǎng)頁(yè)制作和功能擴(kuò)展;專業(yè)做搜索引擎喜愛的網(wǎng)站,專業(yè)的做網(wǎng)站團(tuán)隊(duì),希望更多企業(yè)前來合作!
在《Effective Java》一書的第 8 條目中,Josh Bloch 將子類化時(shí)滿足 equals 約定這一困難描述為:面向?qū)ο笳Z(yǔ)言中等值關(guān)系的最根本問題。Bloch 這樣寫道:
不存在一種方式,能夠在擴(kuò)展非實(shí)例類并添加值組件的同時(shí),仍然滿足equals的約定。除非你愿意放棄面向?qū)ο蟮某橄笮赃@一優(yōu)點(diǎn)。
《Programming in Scala》一書中的第 28 章提供了一種方法,子類可以對(duì)非實(shí)例類進(jìn)行擴(kuò)展,添加值組件,而同時(shí)滿足 equals 約定。雖然書中提供的那種技巧是用于定義 Scala 類,但一樣適用于 Java 中的 類定義。在本文中,為了講解這種方法,我將使用《Programming in Scala》中相關(guān)章節(jié),改編相關(guān)的文本,并將原書中的 Scala 示例代碼轉(zhuǎn)換為了 Java 代碼。
常見的等值陷阱
Class java.lang.Object 定義了一個(gè) equals 方法,其中的子類可以進(jìn)行改寫(override)。不幸的是,最終的結(jié)果表明,在面向?qū)ο笳Z(yǔ)言中,編寫正確的等值方法相當(dāng)困難。事實(shí)上,在對(duì) Java 代碼的大量正文進(jìn)行研究之后,幾位作者在 2007 年的一份論文中作出如下結(jié)論:幾乎所有 equals 方法的實(shí)現(xiàn)都是錯(cuò)誤的。
這是一個(gè)嚴(yán)重的問題,因?yàn)榈戎捣椒ㄊ呛芏啻a的根本。其一,對(duì)于類型 C,一個(gè)錯(cuò)誤的等值方法可能意味著,你不能可靠地將一個(gè)類型 C 的對(duì)象放入集合中。你可能有兩個(gè)等值的類型 C 元素 elem1、elem2,即“em1.equals(elem2)”輸出 true。然而,在下面的示例中,equals 方法的實(shí)現(xiàn)就是一種常見的錯(cuò)誤:
- Set< C> hashSet = new java.util.HashSet< C>();
- hashSet.add(elem1);
- hashSet.contains(elem2); // 返回 false!
存在四種常見的陷阱,它們都會(huì)在改寫equals時(shí)導(dǎo)致非一致性的行為:
◆使用錯(cuò)誤的原型對(duì)equals進(jìn)行定義。
◆更改equals而未同時(shí)更改 hashCode。
◆對(duì)equals進(jìn)行定義時(shí)涉及可變域(field)。
◆未能成功地將equals定義為等值關(guān)系。
這四種陷阱將在下文中具體講述。
#p#
陷阱 1:使用錯(cuò)誤的原型對(duì)equals進(jìn)行定義
在下面的代碼中,我們將為普通點(diǎn)的類添加一個(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;
- }
- // ...
- }
一個(gè)顯而易見的錯(cuò)誤定義如下:
- // 一個(gè)完全錯(cuò)誤的 equals 定義
- public boolean equals(Point other) {
- return (this.getX() == other.getX() && this.getY() == other.getY());
- }
這種方法的錯(cuò)誤之處是什么?初一看,它可以正常運(yùn)行:
- 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)); // 輸出 false
然而,一旦你將point放入集合中,問題就出來了:
- import java.util.HashSet;
- HashSet< Point> coll = new HashSet< Point>();
- coll.add(p1);
- System.out.println(coll.contains(p2)); // 輸出 false
coll 怎么可能不包含 p2 呢?你已經(jīng)將 p1 添加到其中,而 p1 等于 p2。在下面的互操作中,進(jìn)行比較的點(diǎn)的具體類型被隱藏,這時(shí),導(dǎo)致問題的原因?qū)⑶逦梢?。?p2a 定義為 p2 的別名,但使用的是 Object 類型而不是 Point:
- Object p2a = p2;
現(xiàn)在,如果你重復(fù)第一個(gè)比較,使用別名 p2a 而不是 p2,結(jié)果是:
- System.out.println(p1.equals(p2a)); // 輸出 false
哪里出錯(cuò)了呢?事實(shí)上,由于類型不同,之前指定的 equals 版本并沒有改寫標(biāo)準(zhǔn)方法 equals。下面是在根類 Object 中定義的 equals 方法:
- public boolean equals(Object other)
由于 Point 中的 equals 方法使用 Point 而不是 Object 作為參數(shù),因此,它并未對(duì) Object 中的 equals 進(jìn)行改寫。相反,它只是一種重載的替代方法。Java 中重載由參數(shù)的靜態(tài)類型解析,而不是運(yùn)行時(shí)(run-time)類型。因此,只要參數(shù)的靜態(tài)類型是 Point,就調(diào)用 Point 中的 equals 方法。同樣,如果靜態(tài)參數(shù)是 Object 類型,就調(diào)用 Object 中的 equals 方法。該方法沒有被改寫,因此在對(duì) object 參數(shù)進(jìn)行比較時(shí),仍使用該方法。這就是“p1.equals(p2a)”輸出 false 的原因,即使點(diǎn) p1 和 p2a 具有相同的 x 和 y 值。這也是為什么在 HashSet 中 contains 方法返回 false 的原因。該方法是針對(duì)對(duì)常規(guī)集合進(jìn)行操作,因此它會(huì)調(diào)用 Object 中的常規(guī) equals 方法,而不是 Point 中重載的方法變種。
下面的代碼定義了一個(gè)更好的equals方法:
- // 一個(gè)更好的定義,但仍不是完美的
- @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ù)并輸出一個(gè) boolean 結(jié)果。該方法的實(shí)現(xiàn)使用了 instanceof 和 cast(類型轉(zhuǎn)換)。它首先檢測(cè)其他(other)對(duì)象是否為 Point 類型。如果是,它將對(duì)這 2 個(gè)點(diǎn)的坐標(biāo)進(jìn)行比較,然后返回結(jié)果。否則,輸出為 false。
#p#
陷阱2 :更改equals而未同時(shí)更改hashCode
如果你使用 Point 的最新定義,再次對(duì) p1和 p2a 進(jìn)行比較,將會(huì)得到期望中的結(jié)果:true。但是,如果你重復(fù) HashSet.contains 測(cè)試,結(jié)果仍可能是 false:
- 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í)上,輸出結(jié)果不是百分百確定。你也可能從測(cè)試中得到true值。如果得到的結(jié)果是true,你可以試試另外一些坐標(biāo)為1和2的點(diǎn)。最終,你將會(huì)找到一個(gè)未包含在集合中的點(diǎn)。這里出現(xiàn)錯(cuò)誤的原因是,Point重定義了equals而沒有對(duì)hashCode進(jìn)行重定義。
請(qǐng)注意,上述實(shí)例中的集合為HashSet。這表示,集合中元素被放在由相應(yīng)的散列碼決定的哈希桶(hash bucket)中。在contains測(cè)試中,它首先查找散列桶,然后對(duì)哈希桶中的所有元素和指定元素進(jìn)行比較。現(xiàn)在,Point類的最新版本確實(shí)對(duì)equals進(jìn)行了重定義,但它沒有同時(shí)對(duì)hashCode進(jìn)行重定義。所以 hashCode 仍然保持 Object 類中其版本的值:分配對(duì)象地址的某種變化格式。p1 和 p2 的散列碼幾乎肯定是不同,即使這兩個(gè)點(diǎn)的域(field)是相同的。不同的散列碼意味著集合中散列桶具有較高概率的非重復(fù)性。contains 測(cè)試將根據(jù) p2 的散列碼在相應(yīng)的散列桶中查找匹配的元素。大多數(shù)情況下,點(diǎn) p1 會(huì)位于另一個(gè)散列桶中,因此絕不會(huì)找到它。p1 和 p2 有可能很偶然地位于同一散列桶中。對(duì)于這種情況,測(cè)試將返回ture 值。
問題在于,Point 的上次實(shí)現(xiàn)違法了Object 類中定義的hashCode約定:
如果兩個(gè)對(duì)象根據(jù)equals(Object) 方法是等值的,那么對(duì)兩個(gè)對(duì)象中任何一個(gè)調(diào)用 hashCode 方法都必須得到相同的整型結(jié)果。
事實(shí)上,在Java中,通常應(yīng)同時(shí)對(duì) hashCode 和equals進(jìn)行重定義,這一事實(shí)是廣為人知的。此外,hashCode 可能僅依賴equals所依賴的域。對(duì)于 Point 類,以下將是一個(gè)合適的 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)中的一種。將常量 41 加到一個(gè)整型域 x 上,所得結(jié)果再乘以素?cái)?shù) 41,然后在加上另一個(gè)整型域 y。這樣就可以提供合理分布的散列碼,而運(yùn)行時(shí)間和代碼大小也會(huì)降低。
在定義與 Point 相似的類時(shí),添加 hashCode 解決了等值的問題。但是,還有其他的問題需要注意。
#p#
陷阱 3 :對(duì)equals進(jìn)行定義時(shí)涉及可變域
以下對(duì) Point 類進(jìn)行一項(xiàng)細(xì)微的修改:
- 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) {
- 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 類型,同時(shí)添加了兩個(gè)集合方法,允許用戶更改 x 和 y 值?,F(xiàn)在,equals和 hashCode 方法的定義涉及了這些可變域,因此域更改時(shí)它們的結(jié)果也將改變。一旦你將點(diǎn)放入集合中,這會(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)在,如果更改點(diǎn) p 中的域,集合還將包含該點(diǎn)嗎? 我們來試試下面的代碼:
- p.setX(p.getX() + 1);
- System.out.println(coll.contains(p)); // (很可能)輸出 false
這看起來很奇怪。p 到哪里去了?如果你對(duì)集合的 iterator 是否包含 p 進(jìn)行測(cè)試,會(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
此處的集合不包含 p,但 p 卻在該集合的元素之中!發(fā)生了什么事呢?在更改 x 域之后,點(diǎn) p 最后被放在了該集合 coll 下錯(cuò)誤的散列桶中。也就是,其初始散列桶與散列碼的新值已不再對(duì)應(yīng)。在某種意義上可以說,點(diǎn) p 在集合 coll 中消失了,即使它仍然是集合中元素。
從這個(gè)示例得出的教訓(xùn)就是,當(dāng)equals和 hashCode 取決于可變狀態(tài)時(shí),可能會(huì)為用戶帶來問題。如果他們將這種對(duì)象放入集合中,必須小心,不要修改決定性的狀態(tài)。而這是很棘手的。如果你現(xiàn)在需要進(jìn)行一個(gè)比較,要考慮到對(duì)象的當(dāng)前狀態(tài),通常不應(yīng)直接使用 equals,而是使用其他命名。 對(duì)于 Point 的上一個(gè)定義,更為可取的是省略 hashCode 的重定義,并且命名比較方法 equalContents,或者使用其他不同于equals的命名。 這樣,Point 將能夠繼承equals和 hashCode 的缺省實(shí)現(xiàn)。
#p#
陷阱 4:未能成功地將equals定義為等值關(guān)系
Object 中equals的約定指出 equals 必須實(shí)現(xiàn)非空對(duì)象的等值關(guān)系:
◆自反性:對(duì)于如何非空值 x,表達(dá)式 x.equals(x) 應(yīng)返回true。
◆對(duì)稱性:對(duì)于任何非空值:x 和 y,x.equals(y) 應(yīng)返回true,當(dāng)且僅當(dāng) y.equals(x) 返回 true。
◆傳遞性:對(duì)于任何非空值 x、y、z,如果 x.equals(y)返回 true 并且 y.equals(z) 返回 true,那么x.equals(z) 應(yīng)返回 true。
◆一致性:對(duì)于任何非空值:x 和 y,多次調(diào)用 x.equals(y)應(yīng)始終返回 true 或始終返回 false,如果對(duì)象的equals比較中所用信息未被修改。
◆對(duì)于任何非空值 x,x.equals(null) 應(yīng)返回 false。
目前,對(duì)于 Point 類所使用的equals定義滿足了equals的約定。然而,一旦涉及子類,事情將變得更加復(fù)雜。比如說,Point 有一個(gè)子類 ColoredPoint,其中添加了一個(gè) Color 類型的域 color。假定將 Color 定義為枚舉類型:
- public enum Color {
- RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET;
- }
ColoredPoint 改寫 equals,并添加新的 color 域:
- public class ColoredPoint extends Point { // 問題:equals 不對(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;
- }
- }
很多程序員都可能這樣編寫代碼。請(qǐng)注意,在這種情況下,類 ColoredPoint 不必改寫 hashCode。因?yàn)?ColoredPoint 的equals的新定義比 Point 中被改寫的定義更為嚴(yán)格(意味著等值的對(duì)象更少),hashCode 的約定仍然有效。 如果兩個(gè)顏色點(diǎn)是相等的,它們必須具有相同的坐標(biāo),因此也已保證它們的散列碼是相等的。
以類 ColoredPoint 本身為例,equals的定義看起來沒問題。但,一旦普通點(diǎn)和顏色點(diǎn)混合著一起時(shí),equals的約定就將被破壞。 例如:
- 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
相等比較“pequalscp”將調(diào)用 p 的equals方法,已在類 Point 中定義。該方法僅考慮兩個(gè)點(diǎn)的坐標(biāo)。因此,相等比較輸出 true 值。而另一方面,相等比較“cp equals p”調(diào)用 cp 的 equals 方法,其中類 ColoredPoint 中已定義。該方法返回 false,因?yàn)?p 不是 ColoredPoint。該方法返回 false,因?yàn)?p 不是 ColoredPoint。 因此,equals定義的關(guān)系不是對(duì)稱的。
對(duì)稱的缺失將為集合造成意想不到的后果。下面為一個(gè)示例:
- 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 是相等的,一個(gè) contains 測(cè)試成功,而另一個(gè)卻失敗。
如何更改equals的 定義可以讓它變?yōu)閷?duì)稱?基本上有兩種方式。您可以使關(guān)系更一般或更嚴(yán)格。使它更加一般意味著對(duì)兩個(gè)對(duì)象,a 和 b 被認(rèn)為是相等的,如果比較 a 和 b 或 b 和 a 輸出 true。以下為完成該功能的代碼:
- public class ColoredPoint extends Point { // 有問題:equals 不具有傳遞性
- 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ì)象是 Point 而不是 ColoredPoint,該方法將使用 Point 的equals方法。這樣就可以取得預(yù)期的效果,使equals具有對(duì)稱性?,F(xiàn)在,“cp.equals(p)”和“p.equals(cp)”都返回 true。 然而,equals的約定還是被打破了?,F(xiàn)在的問題是,新的關(guān)系不再具有傳遞性!為了演示這個(gè)問題,下面進(jìn)行一系列的聲明。定義一個(gè)點(diǎn)和兩個(gè)不同色的顏色點(diǎn),所有點(diǎn)在同一位置:
- ColoredPoint redP = new ColoredPoint(1, 2, Color.RED);
- ColoredPoint blueP = new ColoredPoint(1, 2, Color.BLUE);
單獨(dú)來看,redp 等于 p 并且 p 等于 bluep:
- System.out.println(redP.equals(p)); // 輸出 true
- System.out.println(p.equals(blueP)); // 輸出 true
然而,比較 redP 和 blueP,輸出 false:
- System.out.println(redP.equals(blueP)); // 輸出 false
因此,這違反了equals約定中的傳遞性子條款。
讓equals關(guān)系更一般看來是死路一條。下面我們?cè)囋囎屗鼑?yán)格。使equals更嚴(yán)格的一個(gè)方法是:不同類的對(duì)象,不同地對(duì)待。通過修改類 Point 和 ColoredPoint 中的equals方法來實(shí)現(xiàn)。 在類 Point 中,可以添加一個(gè)額外的比較,用于檢查其他點(diǎn)的運(yùn)行時(shí)類是否與這個(gè)點(diǎn)的類相同,代碼如下:
- // 技術(shù)上有效,但仍不能令人滿意的 equals 方法
- 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());
- }
- }
然后,你就可以將類 ColoredPoint 的實(shí)現(xiàn)恢復(fù)為之前違反了對(duì)稱性要求的版本:
- 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í)例被認(rèn)為與系統(tǒng)類的其他實(shí)例是相等的,僅當(dāng)對(duì)象具有相同的坐標(biāo),并且具有相同的運(yùn)行時(shí)類,即每個(gè)對(duì)象 .getClass() 返回相同的值。 新的定義可以滿足對(duì)象性和傳遞性的要求,因?yàn)楝F(xiàn)在對(duì)不同類之間的每次對(duì)比都將返回 false。因此,顏色點(diǎn)永遠(yuǎn)不會(huì)與普通點(diǎn)相等。這種約定看起來是合理的,當(dāng)有人會(huì)指出新的定義太過嚴(yán)格了。
下面使用稍微有點(diǎn)繞的方式定義位于坐標(biāo)(1, 2)上的點(diǎn):
- Point pAnon = new Point(1, 1) {
- @Override public int getY() {
- return 2;
- }
- };
pAnon 等于 p 嗎?答案是否定的,因?yàn)榕c p 和 pAnon 關(guān)聯(lián)的 java.lang.Class 對(duì)象是不同的。對(duì)于 p 是 Point 類,而對(duì)于 pAnon,它是 Point 的一個(gè)匿名子類。 但顯然,pAnon 只是位于坐標(biāo)(1, 2)上的另一個(gè)點(diǎn)。 認(rèn)為它與 p 不同,看起來并不合理。
#p#
明智的方式:canEqual 方法
從以上各種情況,看起來我們進(jìn)退兩難。是否存在一種明智的方式,在類層次結(jié)構(gòu)的多個(gè)分層中對(duì)等值比較進(jìn)行重定義,而同時(shí)滿足其約定?事實(shí)上,有這樣一種方式,但它需要另一個(gè)方法來重定義equals和 hashCode。這個(gè)想法是,只要類重定義 equals(和 hashCode),它就應(yīng)該同時(shí)顯式地聲明,該類的所有對(duì)象與使用不同等值方法的超類中的對(duì)象,絕對(duì)不會(huì)相等。通過對(duì)重定義equals的每個(gè)類添加方法 canEqual 就可以實(shí)現(xiàn)。以下為該方法的原型:
- public boolean canEqual(Object other)
當(dāng)其他(other)對(duì)象是(重)定義了 canEqual 的類的實(shí)例時(shí),該方法應(yīng)返回 true,或者返回 false。它從equals中調(diào)用,以確保這些對(duì)象使用2種方式都是可比較的。下面是類 Point 新的也是最后的一個(gè)實(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方法包含了一個(gè)附加的要求,由 canEqual 方法決定,其他(other)對(duì)象可以等于這個(gè)(this)對(duì)象。Point 中的 canEqual 實(shí)現(xiàn)聲明所有 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 的新定義滿足了equals的約定。 等值是對(duì)稱和可傳遞的。將一個(gè) Point 與 ColoredPoint 比較,總會(huì)輸出 false。事實(shí)上,任何普通點(diǎn) p 和顏色點(diǎn) cp,“p.equals(cp)”將返回 false ,因?yàn)椤癱p.canEqual(p)”將返回 false。反向進(jìn)行比較,“cp.equals(p)”也將返回 false ,因?yàn)?p 的確不是ColoredPoint,所以 ColoredPoint 中equals正文中第一個(gè) instanceof 檢查將失敗。
另一方面, Point 的不同子類的實(shí)例可以是相等的,只要這些類沒有重定義等值比較方法。例如,使用新的類定義,p 和 pAnon 的比較結(jié)果為 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 coll = new java.util.HashSet ();
- 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)用 canEqual,那么實(shí)現(xiàn)子類的程序員可以決定他們的子類是否與超類的實(shí)例相等。由于 ColoredPoint 改寫了 canEqual,例如,顏色點(diǎn)可能遠(yuǎn)不會(huì)與普通點(diǎn)相等。但由于 pAnon 中引用的匿名子類并未改寫 canEqual,其實(shí)例可以與 Point 實(shí)例相等。
對(duì) canEqual 方式,一種可能的批評(píng)是,它違反了里氏替換原則(Liskov Substitution Principle:縮寫 LSP)。例如,通過比較運(yùn)行時(shí)類來實(shí)現(xiàn) equals 的技巧,被認(rèn)為違反了 LSP,因?yàn)樵摷记蓪?dǎo)致無法定義這樣一個(gè)子類:其實(shí)例可以等于超類的實(shí)例。其推理思路是,LSP 聲明:在需要超類實(shí)例的地方,你應(yīng)能夠使用(調(diào)換)子類實(shí)例。但是,在之前的實(shí)例中,“coll.contains(cp)”返回 false,即使 cp 的 x 和 y 值與集合中點(diǎn)相匹配。因此,看起來它可能像是違反了 LSP,因?yàn)槟悴荒苤谐霈F(xiàn) Point 的地方使用 ColoredPoint。但是,我們認(rèn)為這是一種錯(cuò)誤的解釋,因?yàn)?LSP 并不要求子類的行為與超類完全相同,而只是它的行為方式能夠滿足超類的約定。
編寫equals方法對(duì)運(yùn)行時(shí)類進(jìn)行比較的問題不在于它違反了 LSP,而是它沒有為你提供一種方式,用來創(chuàng)建其實(shí)例與超類實(shí)例相等的子類。例如,如果我們之前的實(shí)例中使用運(yùn)行時(shí)類的技巧,“coll.contains(pAnon)”將返回 false,而這不是我們想要的。相反,我們真的想要“coll.contains(cp)”返回 false,因?yàn)橥ㄟ^改寫 ColoredPoint 中的 equals,我們基本上是在表示,一個(gè)位于坐標(biāo)(1, 2)上的深藍(lán)色點(diǎn)與位于(1, 2)的非顏色點(diǎn)并不相同。因此,在前面的例子中,我們可以將兩個(gè)不同 Point 子類實(shí)例傳遞到集合的 contains 方法中,并且得到兩個(gè)不同的結(jié)果,兩個(gè)都是正確的。
原文:How to Write an Equality Method in Java
作者:Martin Odersky,Lex Spoon,以及Bill Venners
譯者:司馬牽牛
標(biāo)題名稱:Java:所有的equals方法實(shí)現(xiàn)都是錯(cuò)誤的?
分享路徑:http://www.5511xx.com/article/djiihsh.html


咨詢
建站咨詢
