覆寫(xiě)equals方法看上去簡(jiǎn)單览露,但是有許多方式弄錯(cuò)這個(gè)問(wèn)題,而且后果可能?chē)?yán)重搞挣。避免這個(gè)問(wèn)題的最容易的方式是,不要覆寫(xiě)equals方法音羞,這種情況下囱桨,這個(gè)類的每個(gè)實(shí)例只等于它本身。如果下列條件之一符合嗅绰,就是做對(duì)的事情:
類的每個(gè)實(shí)例本質(zhì)上是唯一的舍肠。對(duì)于這些類,比如表示激活實(shí)體(active entity)而不是值的Thread窘面,是正確的翠语。對(duì)于這些類,Object提供的equals實(shí)現(xiàn)恰好有正確的行為财边。
沒(méi)有必要為類提供一個(gè)“邏輯上相等”的檢測(cè)肌括。比如,java.util.regex.Pattern本可以覆寫(xiě)equals檢測(cè)兩個(gè)Pattern實(shí)例是否恰好表示同一個(gè)正則表達(dá)式酣难,但是設(shè)計(jì)者認(rèn)為客戶端沒(méi)必要或者不想有這個(gè)功能谍夭。在這些情況下,從Object繼承的equals實(shí)現(xiàn)是合適的憨募。
一個(gè)超類已經(jīng)有覆寫(xiě)的equals紧索,而且這個(gè)超類行為對(duì)于這個(gè)類來(lái)說(shuō)是合適的。比如菜谣,Set的大多數(shù)實(shí)現(xiàn)從AbstractSet繼承它們的equals實(shí)現(xiàn)珠漂,List實(shí)現(xiàn)是從AbstractList,Map實(shí)現(xiàn)是從AbstractMap尾膊。
類是私有的或者包私有的甘磨,而且你肯定它的equals方法永遠(yuǎn)不會(huì)調(diào)用。如果你是極端風(fēng)險(xiǎn)規(guī)避的眯停,你可以覆寫(xiě)equals方法保證它不會(huì)意外調(diào)用:
@Override public boolean equals(Object o) {
throw new AssertionError(); // 方法不會(huì)被調(diào)用
}
那么什么時(shí)候覆寫(xiě)方法合適呢?邏輯相等(logical equality)不同于單單的對(duì)象標(biāo)識(shí)卿泽,當(dāng)類有這個(gè)概念莺债,而且超類還沒(méi)有覆寫(xiě)equals滋觉,這個(gè)時(shí)候是合適的。這通常是值類(value class)這種情況齐邦。一個(gè)值類僅僅是表示值的類椎侠,比如Integer或者String。程序員用equals方法比較值類的引用措拇,期望找出它們是否在邏輯上相等我纪,而不是它們是否引用同一個(gè)對(duì)象。覆寫(xiě)equals方法不僅僅對(duì)于符合程序員期望是必須的丐吓,而且它也使得實(shí)例作為map的鍵或者set的元素是可預(yù)測(cè)的和合理的行為浅悉。
不需要覆寫(xiě)equals方法的一種值類是這樣的類,它使用實(shí)例控制(條目1)保證每個(gè)值最多存在一個(gè)對(duì)象券犁。Enum類型(條目34)屬于這一類术健。對(duì)于這些類,邏輯相等和對(duì)象標(biāo)識(shí)是相同的粘衬,所以
Object的equals方法荞估,它的作用相當(dāng)于邏輯的equals方法。
當(dāng)你覆寫(xiě)equals方法時(shí)稚新,你必須遵守它的通用協(xié)定勘伺。下面是來(lái)自O(shè)bject文檔上的協(xié)定:
equals方法實(shí)現(xiàn)了等價(jià)關(guān)系(equivalence relation)。它有這些屬性:
- 反身性:對(duì)于任何非空的引用值x褂删,x.equals(x)必須返回真飞醉。
- 對(duì)稱性:對(duì)于任何非空引用值x和y,當(dāng)且僅當(dāng)y.equals(x)返回真笤妙,x.equals(y)必須返回真冒掌。
- 傳遞性:對(duì)于任何非空引用值x、y和z蹲盘,如果x.equals(y)返回真股毫,而且y.equals(z)返回真,那么x.equals(z)返回真召衔。
- 一致性:對(duì)于任何非空引用值x铃诬、y和z,x.equals(y)的多次調(diào)用必須一致返回真苍凛,或者一致返回假趣席,假如用在equals比較上的信息沒(méi)有改變。
- 對(duì)于任何非空引用值x醇蝴,x.equals(null)必須返回假宣肚。
除非你熟悉數(shù)學(xué),這看上去可能有點(diǎn)嚇人悠栓,但是不要忽略它霉涨。如果違反了它按价,你可能會(huì)發(fā)現(xiàn),你的程序表現(xiàn)不穩(wěn)定或者崩潰笙瑟,有可能非常難于確定失敗的來(lái)源楼镐。用John Donne的話說(shuō),類不是一個(gè)孤島往枷。一個(gè)類的實(shí)例經(jīng)常傳遞給另外一個(gè)框产。許多類,包括所有的集合類错洁,依賴于傳入的對(duì)象秉宿,這些對(duì)象遵從于equals協(xié)定。
既然你已經(jīng)知道違反equals協(xié)定的危險(xiǎn)墓臭,那么讓我們仔細(xì)檢查這個(gè)協(xié)定蘸鲸。好消息是,盡管表面如此窿锉,它實(shí)際上不是特別復(fù)雜酌摇。一旦你理解了它,遵守它是不難的嗡载。
那么什么是等價(jià)關(guān)系呢窑多?大約地講,它是一個(gè)操作子洼滚,它把一個(gè)集的元素劃分到子集埂息,這個(gè)子集里面的元素被認(rèn)為同另外一個(gè)是相等的。這些子集也稱為相等類遥巴。對(duì)于一個(gè)有用的equals方法千康,每個(gè)相等類里面的所有元素,在用戶的角度看铲掐,必須是可交換的∈捌現(xiàn)在讓我們依次檢查這五項(xiàng)要求:
反身性:第一個(gè)要求說(shuō),一個(gè)對(duì)象必須和自身相等摆霉。很難想象無(wú)意違反了這個(gè)要求豪椿。如果你違反了它,然后把你的類的對(duì)象添加到一個(gè)集中携栋,contains可能會(huì)說(shuō)搭盾,這個(gè)集不含有你剛才添加的實(shí)例。
對(duì)稱性:第二個(gè)要求說(shuō)婉支,任意兩個(gè)對(duì)象必須對(duì)它們是否是相等的要達(dá)成一致鸯隅。不像第一個(gè)要求,不難想象無(wú)意中違反這個(gè)要求向挖。比如滋迈,考慮如下的類霎奢,它實(shí)現(xiàn)了大小寫(xiě)不敏感的字符串。這個(gè)字符串的大小寫(xiě)通過(guò)toString保存著饼灿,但是在equals比較中被忽略:
// 已破壞 - 違反了對(duì)稱性!
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requireNonNull(s);
}
// 已破壞 - 違反了對(duì)稱性!
@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;
}
... // 其余省略
}
這個(gè)類中出于好意的equals方法,徒勞地嘗試和普通的字符串互操作帝美。讓我們假設(shè)碍彭,我們有一個(gè)大小寫(xiě)不敏感的字符串,還有一個(gè)普通的字符串:
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
正如所料悼潭,cis.equals(s)返回真庇忌,問(wèn)題在于,當(dāng)CaseInsensitiveString的equals方法知道普通對(duì)象舰褪,但是String的equals方法不知道大小寫(xiě)不敏感的字符串皆疹。所以,s.equals(cis)返回假占拍,一個(gè)明顯的對(duì)稱性的違反略就。假設(shè)你把大小寫(xiě)不敏感的字符串放入一個(gè)集:
List<CaseInsensitiveString> list = new ArrayList<>();
list.add(cis);
這時(shí)候,list.contains(s)返回什么呢晃酒?誰(shuí)知道呢表牢?在當(dāng)前的OpenJDK實(shí)現(xiàn)中,碰巧返回假贝次,但那僅僅是一個(gè)人工實(shí)現(xiàn)崔兴。在另外一個(gè)實(shí)現(xiàn)中,它可能同樣容易地返回真蛔翅,或者拋出一個(gè)運(yùn)行時(shí)錯(cuò)誤敲茄。如果你違反了equals協(xié)定,你簡(jiǎn)直不知道山析,其他對(duì)象面對(duì)你的對(duì)象時(shí)是什么行為堰燎。
為了消除這個(gè)問(wèn)題,僅需移除與String的equals方法交互這個(gè)錯(cuò)誤的嘗試盖腿。一旦這樣做了爽待,你可以重構(gòu)這個(gè)方法到一個(gè)簡(jiǎn)單的返回語(yǔ)句:
@Override public boolean equals(Object o) {
return o instanceof CaseInsensitiveString &&
((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
- 傳遞性:equals協(xié)定的第三個(gè)要求說(shuō),如果一個(gè)對(duì)象和第二個(gè)相等翩腐,而且第二個(gè)對(duì)象和第三個(gè)相等鸟款,那么第一個(gè)對(duì)象必須和第三個(gè)相等。再次不難想象無(wú)意違反這個(gè)要求茂卦『问玻考慮一個(gè)子類的情形,這個(gè)子類添加一個(gè)新值組件(value component)到它的超類等龙。換句話說(shuō)处渣,子類添加一些信息伶贰,影響了equals比較。讓我們以簡(jiǎn)單不可變的二維整形點(diǎn)類開(kāi)始:
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;
}
...// 其余省略
}
假設(shè)你想擴(kuò)展這個(gè)類罐栈,給一個(gè)點(diǎn)添加顏色的概念:
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y); this.color = color;
}
...// 其余省略
}
equals方法看起來(lái)怎么樣呢黍衙?如果你完全忽視它,繼承于Point的實(shí)現(xiàn)和顏色信息在equals比較時(shí)忽略了荠诬。雖然這不違反equals協(xié)定琅翻,但是明顯是不可接受的。假設(shè)你寫(xiě)了一個(gè)equals方法柑贞,只要它的參數(shù)是另外一個(gè)相同位置和顏色的顏色點(diǎn)方椎,就返回真:
// 已破壞 - 違反對(duì)稱性!
@Override public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
這個(gè)方法的問(wèn)題是,當(dāng)一個(gè)點(diǎn)和一個(gè)顏色點(diǎn)比較時(shí)钧嘶,或者相反棠众,可能有不同的結(jié)果。前者的比較忽略了顏色有决,而后者的比較總是返回假闸拿,因?yàn)閰?shù)的類型是不正確的。為了使這個(gè)更加具體疮薇,讓我們創(chuàng)建一個(gè)點(diǎn)和一個(gè)顏色點(diǎn):
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
于是胸墙,p.equals(cp)返回真,然而cp.equals(p)返回假按咒。當(dāng)進(jìn)行“混合比較”是迟隅,你可能使用ColorPoint.equals時(shí)忽略顏色,來(lái)試著解決這個(gè)問(wèn)題:
// 已破壞 - 違反傳遞性!
@Override public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
// 如果o是個(gè)通常的Point励七,進(jìn)行無(wú)關(guān)顏色的比較
if (!(o instanceof ColorPoint))
return o.equals(this);
// o是ColorPoint類型; 進(jìn)行全面的比較
return super.equals(o) && ((ColorPoint) o).color == color;
}
這個(gè)方法提供了對(duì)稱性智袭,但是是以傳遞性為代價(jià):
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
現(xiàn)在p1.equals(p2)和p2.equals(p3)返回真,而p1.equals(p3)返回假掠抬,這個(gè)明顯的傳遞性違反吼野。前面兩個(gè)比較是無(wú)關(guān)顏色的,而第三個(gè)考慮了顏色两波。
而且瞳步,這個(gè)方法可能造成無(wú)限遞歸:假設(shè)有兩個(gè)Point子類,比如說(shuō)ColorPoint和SmellPoint腰奋,每個(gè)子類都有這樣的equals方法单起。然后myColorPoint.equals(mySmellPoint)的調(diào)用將拋出StackOverflowError。
那么解決方案是什么呢劣坊?結(jié)果是嘀倒,在面向?qū)ο笳Z(yǔ)言中,這是一個(gè)等價(jià)關(guān)系的基本問(wèn)題。當(dāng)維護(hù)equals協(xié)定時(shí)测蘑,沒(méi)辦法擴(kuò)展一個(gè)不可實(shí)例化的類和添加一個(gè)值組件灌危,除非你愿意放棄面向?qū)ο蟮某橄笮浴?/p>
你可能聽(tīng)說(shuō)過(guò),可以擴(kuò)展一個(gè)不可實(shí)例化的類和添加一個(gè)值組件碳胳,而且以getClass檢測(cè)代替instanceof檢測(cè)的方式維護(hù)equals協(xié)定:
// 已破壞 - 違反了里氏代換原則
@Override public boolean equals(Object o) {
if (o == null || o.getClass() != getClass())
return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}
只有在它們有相同實(shí)現(xiàn)類的時(shí)候勇蝙,才有相等對(duì)象的效果。這可能不算太糟固逗,但是結(jié)果是不可接受的:Point子類的實(shí)例仍然一個(gè)Point浅蚪,而且仍然需要起到它的作用,但是烫罩, 如果你使用這個(gè)方法,它不能夠起這樣的作用洽故。假設(shè)我們想要編寫(xiě)一個(gè)方法贝攒,辨別一個(gè)點(diǎn)是否在一個(gè)單元圓上。下面是我們可以這么做的一種方式:
// 初始化單位元时甚,包含單元圓上的所有點(diǎn)
private static final Set<Point> unitCircle = Set.of(
new Point( 1, 0), new Point( 0, 1),
new Point(-1, 0), new Point( 0, -1));
public static boolean onUnitCircle(Point p) {
return unitCircle.contains(p);
}
雖然這可能不是最快的方式實(shí)現(xiàn)這個(gè)功能隘弊,但是它工作良好。假設(shè)你用某種簡(jiǎn)單的方式擴(kuò)展Point荒适,而不會(huì)添加一個(gè)值組件梨熙,即,通過(guò)它的構(gòu)造子跟蹤創(chuàng)建了多少個(gè)實(shí)例:
public class CounterPoint extends Point {
private static final AtomicInteger counter = new AtomicInteger();
public CounterPoint(int x, int y) {
super(x, y);
counter.incrementAndGet();
}
public static int numberCreated() {
return counter.get();
}
}
里氏代換原則說(shuō)刀诬,一個(gè)類型的任何重要屬性也應(yīng)該對(duì)所有它的子類有效咽扇,以便為類型編寫(xiě)的任意方法,同樣應(yīng)該在它的子類上工作良好 [Liskov87]陕壹。這是我們更早聲稱的正式說(shuō)明质欲,這個(gè)聲稱說(shuō),Point子類糠馆,比如CounterPoint嘶伟,仍然是一個(gè)Point,而且必須充當(dāng)它的作用又碌。但是九昧,假設(shè)我們傳遞CounterPoint到onUnitCircle。如果Point類使用基于getClass的equals方法毕匀,那么onUnitCircle方法將返回假铸鹰,不管CounterPoint實(shí)例的x和y坐標(biāo)。正是如此期揪,因?yàn)榇蠖鄶?shù)數(shù)據(jù)集掉奄,包括被CounterPoint使用的HashSet,用equals方法來(lái)檢測(cè)包含,而且CounterPoint實(shí)例不等于任何Point姓建。然而诞仓,如果你使用恰當(dāng)?shù)幕趇nstanceof的Point的equals方法,那么當(dāng)面對(duì)CounterPoint實(shí)例時(shí)速兔,同一個(gè)onUnitCircle方法工作良好墅拭。
雖然擴(kuò)展不可實(shí)例化的類和添加值組件是不令人滿意的方式,但是有個(gè)好的變通方案:根據(jù)條目18(組合優(yōu)于繼承)的建議涣狗,不是讓ColorPoint擴(kuò)展Point谍婉,而是給ColorPoint一個(gè)私有的Point域和一個(gè)公開(kāi)的視圖方法(view method)(條目6),這個(gè)方法返回像這個(gè)顏色點(diǎn)同樣位置的一個(gè)點(diǎn):
// 添加一個(gè)值組件镀钓,而沒(méi)有違反equals協(xié)定
public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color) {
point = new Point(x, y);
this.color = Objects.requireNonNull(color);
}
/**
* 返回這個(gè)顏色點(diǎn)的Point的視圖
*/
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);
}
...// 其余省略
}
在Java平臺(tái)庫(kù)里有些類確實(shí)擴(kuò)展一個(gè)不可實(shí)例化的類穗熬,而且添加了一個(gè)值類型。比如丁溅,java.sql.Timestamp擴(kuò)展了java.util.Date唤蔗,而且添加了nanoseconds域。Timestamp的equals實(shí)現(xiàn)確實(shí)違反了對(duì)稱性而且可能造成不穩(wěn)定的行為窟赏,如果Timestamp和Date對(duì)象在同一個(gè)數(shù)據(jù)集中或者另外的混用妓柜。只要你把它們分開(kāi),你就不會(huì)遇到麻煩涯穷,但是沒(méi)有任何事情可以阻止你混用它們棍掐,而且造成的錯(cuò)誤很難調(diào)試。Timestamp類的行為是一個(gè)錯(cuò)誤拷况,不應(yīng)該模仿作煌。
注意,你可以添加值組件到抽象類的一個(gè)子類中蝠嘉,而沒(méi)有違反equals協(xié)定最疆。這是重要的,對(duì)于根據(jù)在條目23(類層級(jí)優(yōu)于標(biāo)記類)的建議蚤告,你獲取到這種類層級(jí)努酸。比如你可以有個(gè)沒(méi)有值組件的抽象Shape類,Circle子類添加了一個(gè)radius域杜恰,而且Rectangle子類添加了長(zhǎng)和寬的域获诈。只要不可能直接創(chuàng)建一個(gè)超類實(shí)例,前面展示的這種問(wèn)題不會(huì)發(fā)生心褐。
- 一致性:equals協(xié)定的第四個(gè)要求是舔涎,如果兩個(gè)對(duì)象是相等的,那么它們所有時(shí)間都必須相等逗爹,除非其中之一或者兩者被修改了亡嫌。換句話說(shuō)嚎于,可變對(duì)象在不同的時(shí)間可能等于不同的對(duì)象,然而不可變對(duì)象是不能的挟冠。當(dāng)你編寫(xiě)一個(gè)類于购,認(rèn)真思考它是否應(yīng)該是不可變的(條目17)。如果你斷定它應(yīng)該知染,確保你的equals方法限制了相等對(duì)象永遠(yuǎn)相等肋僧,不相等對(duì)象永遠(yuǎn)不相等。
不管一個(gè)對(duì)象是否可變控淡,不要編寫(xiě)一個(gè)依賴不可靠資源的equals方法嫌吠。如果你違反了這個(gè)禁令,滿足一致性是極其困難的掺炭。比如辫诅,java.net.URL的equals方法依賴于與主機(jī)IP地址有關(guān)系的URL的比較。翻譯主機(jī)名到IP地址涧狮,可能需要網(wǎng)絡(luò)連接泥栖,而且不能夠保證隨著時(shí)間變化而產(chǎn)生相同的結(jié)果。這個(gè)可能造成URL的equals方法違反了equals協(xié)定勋篓,而且在實(shí)際中引發(fā)問(wèn)題。URL的equals方法的行為是一個(gè)大的錯(cuò)誤魏割,不應(yīng)該模仿譬嚣。不幸的是,由于兼容性要求钞它,這不能夠改變拜银。為了避免這樣的問(wèn)題,equals方法應(yīng)該僅僅對(duì)駐留內(nèi)存的對(duì)象遭垛,執(zhí)行確定性的計(jì)算尼桶。
- 非空性: 最后一個(gè)要求缺少官方名字,所以我冒昧地叫它“非空性”锯仪。它是說(shuō)泵督,所有的對(duì)象應(yīng)該不與空相等。雖然難于想象庶喜,對(duì)o.equals(null)的調(diào)用的響應(yīng)小腊,意外返回真,但是不難想象意外拋出一個(gè)NullPointerException久窟。這個(gè)通用協(xié)定禁止這個(gè)秩冈。許多類有equals方法,用一個(gè)顯式的null檢測(cè)防止它:
@Override public boolean equals(Object o) {
if (o == null)
return false;
...
}
這個(gè)檢測(cè)是必要的斥扛。為了相等檢測(cè)它的參數(shù)入问,equals方法必須首先強(qiáng)轉(zhuǎn)它的參數(shù)到一個(gè)正確的類型,使得它的訪問(wèn)器可以被調(diào)用或者它的域可以被訪問(wèn)。在進(jìn)行強(qiáng)轉(zhuǎn)之前芬失,方法必須使用instanceof操作子楣黍,檢查它的參數(shù)是正確的類型:
@Override public boolean equals(Object o) {
if (!(o instanceof MyType))
return false;
MyType mt = (MyType) o;
...
}
如果這個(gè)類型檢查缺少,而且equals方法傳入了一個(gè)錯(cuò)誤的參數(shù)麸折,equals方法應(yīng)該拋出ClassCastException锡凝,這違反了equals協(xié)定。但是instanceof操作子指定返回假垢啼,如果他的第一個(gè)操作數(shù)是空窜锯,不管第二個(gè)操作數(shù)的類型是什么[JLS, 15.20.2]。所以芭析,如果類型檢查返回假锚扎,所以你不需要顯式的空檢查。
綜合前述馁启,下面是一個(gè)高質(zhì)量的equals方法的秘訣:
使用==操作子檢測(cè)參數(shù)是否是這個(gè)對(duì)象的一個(gè)引用驾孔。如果如此,返回真惯疙。這只是一個(gè)性能優(yōu)化翠勉,但是如果對(duì)比可能代價(jià)大時(shí),這件事情是值得做的霉颠。
使用instanceof操作子檢查參數(shù)是否有相同的類型对碌。如果沒(méi)有,返回假蒿偎。通常朽们,正確的類型是一個(gè)類,這個(gè)方法在這個(gè)類里面出現(xiàn)诉位。有時(shí)骑脱,類型是這個(gè)類實(shí)現(xiàn)的某個(gè)接口。如果類實(shí)現(xiàn)了一個(gè)接口苍糠,這個(gè)接口改善了equals協(xié)定叁丧,允許實(shí)現(xiàn)該接口的類之間比較,那么使用接口椿息。Collection接口歹袁,比如Set、List寝优、Map和Map.Entry轿腺,有這個(gè)屬性魄健。
強(qiáng)轉(zhuǎn)參數(shù)到正確的類型拭荤。因?yàn)檫@個(gè)強(qiáng)轉(zhuǎn)之前有instanceof檢測(cè),保證了成功迁杨。
在類里面的每個(gè)“重要”域,檢測(cè)參數(shù)的域是否匹配這個(gè)對(duì)象的相應(yīng)的域凄硼。如果所有這些檢測(cè)通過(guò)铅协,返回真;否則摊沉,返回假狐史。如果步驟2的類型是一個(gè)接口,你必須通過(guò)接口的方法獲取參數(shù)的域说墨;如果類型是類的話骏全,你或許可能直接獲取域,決定于它們的可存取性尼斧。
對(duì)于原始的域姜贡,而且它的類型不是float或者double,使用==操作子比較棺棵;對(duì)于對(duì)象引用的域楼咳,遞歸地調(diào)用equals方法;對(duì)于float域烛恤,使用靜態(tài)的Float.compare(float, float)母怜;對(duì)于double域,使用Double.compare(double, double)缚柏。float和double域的特別處理是必要的糙申,由于Float.NaN, -0.0f和double類似這樣的值。詳細(xì)情況參考JLS 15.21.1或者Float.equals的文檔船惨。雖然你可以用靜態(tài)方法Float.equals和Double.equals對(duì)比f(wàn)loat和double域,但是每次比較缕陕,會(huì)需要自動(dòng)裝箱粱锐,這使得有糟糕的性能。對(duì)于隊(duì)列的域扛邑,使用這些規(guī)則到每個(gè)元素怜浅。如果隊(duì)列域中的每個(gè)元素都重要,使用Arrays.equals方法之一蔬崩。
一些對(duì)象引用域恶座,可能合理地包括空。為了避免NullPointerException的可能性沥阳,使用靜態(tài)方法Objects.equals(Object, Object)跨琳,為相等性檢測(cè)這些域。
對(duì)于一些類桐罕,比如上面的CaseInsensitiveString脉让,域比較比簡(jiǎn)單的相等檢測(cè)復(fù)雜的多桂敛。如果是這個(gè)情況,你可能想要存儲(chǔ)域的標(biāo)準(zhǔn)形式(canonical form)溅潜,這樣术唬,equals方法可以用標(biāo)準(zhǔn)形式進(jìn)行便宜精確的比較,而不是一個(gè)代價(jià)更高的非標(biāo)準(zhǔn)的比較滚澜。這個(gè)技巧對(duì)不可變類是最合適的(條目17)粗仓;如果這個(gè)對(duì)象可以改變,你必須保持標(biāo)準(zhǔn)形式最新设捐。
equals方法的性能可能會(huì)被它里面域的比較順序所影響借浊。為了最好的性能,你應(yīng)該首先比較這樣的域:更可能不同的挡育、對(duì)比代價(jià)更低的巴碗,或者理想地,兩者即寒。你不可以比較不是對(duì)象邏輯狀態(tài)的部分的域橡淆,比如用在synchronize操作上的鎖域。你不需要比較派生的域母赵,這個(gè)可以從“重要的域”計(jì)算出來(lái)逸爵,但是這樣做可以改進(jìn)equals方法的性能。如果派生的域相當(dāng)于整個(gè)對(duì)象概要描述凹嘲,比較失敗時(shí)师倔,比較這個(gè)域?qū)槟闶∠卤容^實(shí)際數(shù)據(jù)的代價(jià)。舉個(gè)例子周蹭,假設(shè)你有一個(gè)Polygon類趋艘,而且你緩存了這個(gè)地方。如果兩個(gè)多邊形有不同的地方凶朗,你不需要麻煩比較它們的邊和頂點(diǎn)瓷胧。
當(dāng)你完成編寫(xiě)equals方法,問(wèn)你自己這個(gè)三個(gè)問(wèn)題:它是對(duì)稱的嗎棚愤?它是可傳遞的嗎搓萧?它是一致的嗎?不要僅僅問(wèn)自己宛畦;編寫(xiě)單元測(cè)試來(lái)檢測(cè)瘸洛,除非你使用AutoValue生成你的equals方法,這種情況下次和,你可以安全地省略檢測(cè)反肋。如果屬性不能夠,那么弄清楚為什么踏施,然后相應(yīng)修改equals方法囚玫。當(dāng)然喧锦,你的equals方法也必須滿足另外兩個(gè)屬性(反身性和非空性),但是這兩個(gè)屬性通常由它們自己處理抓督。
根據(jù)前面的秘籍構(gòu)建的equals方法燃少,用這個(gè)簡(jiǎn)化的PhoneNumber類顯示:
// 典型equals方法的類
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "area code");
this.prefix = rangeCheck(prefix, 999, "prefix");
this.lineNum = rangeCheck(lineNum, 9999, "line num");
}
private static short rangeCheck(int val, int max, String arg) {
if (val < 0 || val > max)
throw new IllegalArgumentException(arg + ": " + val);
return (short) val;
}
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNum == lineNum && pn.prefix == prefix
&& pn.areaCode == areaCode;
}
... // 其余省略
}
下面是一些最后的警告:
當(dāng)你覆寫(xiě)equals時(shí),永遠(yuǎn)覆寫(xiě)hashCode(條目11)
不要自作聰明铃在。如果你簡(jiǎn)單地為相等性檢測(cè)域阵具,遵守equals協(xié)定是不難的。如果你過(guò)度積極尋找等價(jià)定铜,容易陷入麻煩阳液。考慮任何混疊揣炕,通常只是一個(gè)壞主意帘皿。比如,F(xiàn)ile類不應(yīng)該嘗試使同個(gè)文件的符號(hào)鏈接相等畸陡。幸好鹰溜,它沒(méi)有。
在equals聲明中丁恭,不要為Object替代另外的類別曹动。對(duì)于一個(gè)程序員,這不是不常見(jiàn)的牲览,編寫(xiě)一個(gè)equals方法墓陈,看上去這樣,然后花幾個(gè)小時(shí)苦苦考慮為什么沒(méi)有正常工作第献。
// 已破壞 - 參數(shù)類型必須是Object!
public boolean equals(MyClass o) {
...
}
問(wèn)題是贡必,這個(gè)方法沒(méi)有覆寫(xiě)Object.equals,它的參數(shù)是Object類別庸毫,而是相反赊级,重載了它(條目52)。這是不可接受的岔绸,提供這樣一個(gè)“強(qiáng)類型”的equals方法,甚至在正常的方法之外橡伞,因?yàn)樗斐勺宇愔械腛verride注解生成一個(gè)錯(cuò)誤的正值盒揉,而且提供了一個(gè)安全的錯(cuò)覺(jué)。
Override注解的一致使用兑徘,就像這個(gè)條目中一直闡述的刚盈,將阻止你犯這個(gè)錯(cuò)誤(條目40)。這個(gè)equals方法編譯不通過(guò)挂脑,而且這個(gè)錯(cuò)誤信息將告訴什么問(wèn)題藕漱。
// 仍然已破壞欲侮,但是編譯不通過(guò)
@Override public boolean equals(MyClass o) {
...
}
編寫(xiě)和測(cè)試equals(和hashCode)方法是乏味的,而且產(chǎn)生的結(jié)果是無(wú)奇的肋联。手動(dòng)編寫(xiě)和測(cè)試這些方法威蕉,有個(gè)絕佳的替代是使用谷歌的AutoValue開(kāi)源框架,它會(huì)自動(dòng)為你生成這些方法橄仍,由類的注釋觸發(fā)韧涨。在大多數(shù)情況,AutoValue生成的方法和你自己編寫(xiě)的方法本質(zhì)上是相同的侮繁。
同樣虑粥,IDE有工具生成equals和hashcode方法,但是產(chǎn)生的源代碼宪哩,相對(duì)于使用AutoValue娩贷,更加冗長(zhǎng)和更不可讀,不要在類中自動(dòng)地跟蹤改變锁孟,所以需要測(cè)試彬祖。這就是說(shuō),用IDE生成equals(和hashCode)方法罗岖,相對(duì)于手動(dòng)實(shí)現(xiàn)他們涧至,通常是更加可取的,因?yàn)镮DE不會(huì)犯粗心大意的錯(cuò)誤桑包,但是人類會(huì)南蓬。
總之,不要覆寫(xiě)equals方法哑了,除非你不得不:在許多情形下赘方,從Object繼承的實(shí)現(xiàn)恰好是如你所想的。如果你確實(shí)要覆寫(xiě)equals弱左,確保比較所有類的重要域窄陡,而且以一種符合equals協(xié)定的五種條款的方式比較它們。