盡管Object是一個(gè)具體類(lèi)绝页,但是設(shè)計(jì)它主要是為了擴(kuò)展,它所有的非final方法(equals诫咱、hashCode、toString洪灯、clone、finalize)都有明確的通用約定竟痰,因?yàn)樗鼈冊(cè)O(shè)計(jì)成是可覆蓋的(override)签钩,任何一個(gè)類(lèi),它在覆蓋這些方法的時(shí)候坏快,都有責(zé)任遵守這些通用約定铅檩,如果不能做到這點(diǎn),其他依賴(lài)于這些約定的類(lèi)(例如:HashMap和HashSet)就無(wú)法結(jié)合該類(lèi)一起正常運(yùn)作莽鸿。
覆蓋equals方法看起來(lái)似乎很簡(jiǎn)單昧旨,但是有許多覆蓋方式會(huì)導(dǎo)致錯(cuò)誤,并且后果非常嚴(yán)重祥得,最容易避免這類(lèi)問(wèn)題的辦法就是不覆蓋equals方法兔沃,在這種情況下,類(lèi)的每個(gè)實(shí)例都只與它自身相等级及,如果滿(mǎn)足以下任何一個(gè)條件乒疏,這就正是所期望的結(jié)果(就不用覆蓋equals方法):
1.類(lèi)的每個(gè)實(shí)例本質(zhì)上是唯一的
:對(duì)于代表活動(dòng)實(shí)體而不是值的類(lèi)確實(shí)如此,例如Thread饮焦,Object提供的equals實(shí)現(xiàn)對(duì)于這些類(lèi)來(lái)說(shuō)正是正確的行為怕吴。
2.類(lèi)沒(méi)有必要提供“邏輯相等“的測(cè)試功能
。例如:java.util.regex.Pattern可以覆蓋equals方法县踢,已檢查兩個(gè)Pattern實(shí)例是否代表同一個(gè)正則表達(dá)式转绷,但是設(shè)計(jì)者并不認(rèn)為客戶(hù)端需要或者期望這樣的功能。在這類(lèi)情況下硼啤,從Object繼承的得到的equals實(shí)現(xiàn)就足夠了议经。
3.超類(lèi)已經(jīng)覆蓋了equals,超類(lèi)的行為對(duì)于這個(gè)類(lèi)也是合適的丙曙。
例如爸业,大多數(shù)的Set實(shí)現(xiàn)都從AbstractSet繼承equals實(shí)現(xiàn),Map實(shí)現(xiàn)從AbsrtractMap繼承equals實(shí)現(xiàn)亏镰。
4.類(lèi)是私有的扯旷,或者是包級(jí)私有的,可以確保它的equals方法永遠(yuǎn)不會(huì)被調(diào)用
索抓。如果非常想要避免風(fēng)險(xiǎn)钧忽,可以覆蓋equals方法毯炮,并加以控制,以確保不會(huì)被意外調(diào)用
@Override
public boolean equals(Object o){
throw new AssertionError();
}
那么耸黑,什么時(shí)候應(yīng)該覆蓋equals方法呢桃煎?如果類(lèi)具有自己特有的"邏輯相等"概念(不同于對(duì)象等同的概念),而且超類(lèi)還沒(méi)有覆蓋equals方法大刊。這通常屬于值類(lèi)的情形为迈。值類(lèi)僅僅是一個(gè)表示值的類(lèi),例如:Integer或者String缺菌,程序員在利用equals方法來(lái)比較值對(duì)象的引用時(shí)葫辐,希望知道它們?cè)谶壿嬌鲜欠裣嗟龋皇窍窳私馑鼈兪欠裰赶蛲粋€(gè)對(duì)象
伴郁,不僅必須覆蓋equals方法耿战,而且這樣做也使得這個(gè)類(lèi)的實(shí)例可以被用作映射表(map)的鍵(key)或者集合(set)的元素,使映射或者集合表現(xiàn)出逾期的行為焊傅。
有一種"值類(lèi)"不需要覆蓋equals方法剂陡,即用實(shí)例受控確保"每個(gè)值最多只存在一個(gè)對(duì)象"的類(lèi),枚舉類(lèi)型就屬于這種類(lèi)狐胎,對(duì)于這樣的類(lèi)而言鸭栖,邏輯相同與對(duì)象相同是一回事。
在覆蓋equals方法的時(shí)候顽爹,必須遵守通用約定纤泵,下面是約定的內(nèi)容,來(lái)自O(shè)bject的規(guī)范:
自反性
:對(duì)于任何非null的引用值x镜粤,x.equals(x)捏题,必須返回true。
對(duì)稱(chēng)性
:對(duì)于任何非null的引用值x肉渴、y公荧,當(dāng)且僅當(dāng)x.equals(y)返回true時(shí),y.equals(x)也必須返回true同规。
傳遞性
:對(duì)于任何非null的引用值x循狰、y、z券勺,如果x.equals(y)返回true绪钥,y.equals(z)也返回true,那么x.equals(z)也必須返回true关炼。
一致性
:對(duì)于任何非null引用值x程腹、y,只要equals的比較操作在對(duì)象中所用的信息沒(méi)有修改儒拂,多次調(diào)用x.equals(y)就會(huì)一致的返回true寸潦,或者一致的返回false色鸳。
非空性
:對(duì)于任何非null引用值x,x.equals(null)必須返false见转。
這些規(guī)定絕對(duì)不能忽略命雀,如果違反了,就會(huì)發(fā)現(xiàn)程序?qū)?huì)表現(xiàn)不正常斩箫,甚至崩潰吏砂,而且很難找到失敗的根源。一個(gè)類(lèi)的實(shí)例通常會(huì)被頻繁的傳遞給另一個(gè)類(lèi)的實(shí)例乘客,有許多類(lèi)赊抖,包括所有的集合類(lèi)在內(nèi),都依賴(lài)于傳遞給它們的對(duì)象是否遵守了equals約定寨典。
那么什么是等價(jià)關(guān)系呢?不嚴(yán)格的說(shuō)房匆,它是一個(gè)操作符耸成,將一組元素劃分到其元素與另一個(gè)元素等價(jià)的分組中
。這些分組被稱(chēng)作等價(jià)類(lèi)浴鸿。從用戶(hù)的角度看井氢,對(duì)于有用的equals方法,每個(gè)等價(jià)類(lèi)中的所有元素都必須是可交換的≡懒矗現(xiàn)在我們按照順序逐一查看以下5個(gè)要求:
自反性
:第一個(gè)要求僅僅說(shuō)明對(duì)象必須等于其自身花竞,假如違背了這一條,然后把該類(lèi)的實(shí)例添加到集合中掸哑,該集合的contains方法將果斷的告訴你约急,該集合不包含你剛剛添加的元素。
對(duì)稱(chēng)性
:第二個(gè)要求是說(shuō):任何兩個(gè)對(duì)象對(duì)于"它們是否相等"的問(wèn)題都必須保持一致苗分,與第一個(gè)要求不同厌蔽,若無(wú)意中違反了這一條,這種情形倒不難想象摔癣,例如下面的類(lèi)奴饮,它實(shí)現(xiàn)了一個(gè)不區(qū)分大小寫(xiě)的字符串的equals方法。
public class CaseInsensitiveString {
private String str;
public CaseInsensitiveString(String str) {
this.str = str;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CaseInsensitiveString) {
CaseInsensitiveString caseString = (CaseInsensitiveString) obj;
return this.str.equalsIgnoreCase(caseString.str);
}
if (obj instanceof String) {
return this.str.equalsIgnoreCase((String) obj);
}
return false;
}
}
在這個(gè)類(lèi)中择浊,equals方法的意圖非常好戴卜,它企圖與普通的字符串對(duì)象進(jìn)行互操作。假設(shè)我們有一個(gè)不區(qū)分大小寫(xiě)的字符串和一個(gè)普通的字符串琢岩。
CaseInsensitiveString cis = new CaseInsensitiveString("Java");
String s = "java";
不出所料投剥,cis.equals(s)返回true,問(wèn)題在于粘捎,雖然CaseInsensitiveString類(lèi)中的equals方法知道普通的字符串對(duì)象薇缅,但是String類(lèi)中的equals方法并不知道不區(qū)分大小寫(xiě)的字符串危彩,因此,s.equals(cis)返回false泳桦,顯然違反了對(duì)稱(chēng)性汤徽。假設(shè)你把不區(qū)分大小寫(xiě)的字符串對(duì)象放到一個(gè)集合中
List<CaseInsensitiveString> list = new ArrayList<>();
list.contains(s); // false
此時(shí)list.contains(s)會(huì)返回false,一旦違反了equals約定灸撰,當(dāng)其他對(duì)象面對(duì)你的對(duì)象時(shí)谒府,你完全不知道這些對(duì)象的行為會(huì)怎么樣。
為了解決這個(gè)問(wèn)題浮毯,只需把企圖與String互操作的這段代碼從equals中刪除即可完疫。
@Override
public boolean equals(Object obj) {
return obj instanceof CaseInsensitiveString &&
((CaseInsensitiveString) obj).str.equalsIgnoreCase(str);
}
傳遞性
:equals約定的第三個(gè)要求是,如果一個(gè)對(duì)象等于第二個(gè)對(duì)象债蓝,而第二個(gè)對(duì)象又等于第三個(gè)對(duì)象壳鹤,則第三個(gè)對(duì)象一定等于第一個(gè)對(duì)象,同樣的饰迹,無(wú)意識(shí)的違反這條規(guī)則的情形也不難想象芳誓。用子類(lèi)舉例:假設(shè)它將一個(gè)新的值組件添加到了超類(lèi)中,換句話說(shuō)啊鸭,子類(lèi)增加的信息會(huì)影響equals的比較結(jié)果锹淌,我們以一個(gè)簡(jiǎn)單的不可變的二位整數(shù)型Point類(lèi)作為開(kāi)始:
public class Point {
int x;
int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Point) {
Point point = (Point) obj;
return x == point.x && y == point.y;
}
return false;
}
}
假設(shè)想要擴(kuò)展這個(gè)類(lèi),為一個(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方法會(huì)是什么樣呢赠制?如果不完全提供equals方法赂摆,而是直接從Point繼承過(guò)來(lái),在equals作比較的時(shí)候钟些,顏色信息就會(huì)被忽略烟号。雖然這樣做不會(huì)違反equals約定,但是很明顯這是無(wú)法接收的政恍。假設(shè)編寫(xiě)一個(gè)equals方法褥符,只有當(dāng)它的參數(shù)是另一個(gè)有色點(diǎn)并且具有相同的位置和顏色時(shí),才返回true抚垃。
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ColorPoint)) {
return false;
}
ColorPoint colorPoint = (ColorPoint) obj;
return super.equals(obj) && color == colorPoint.color;
}
}
這個(gè)equals方法的問(wèn)題在于喷楣,在比較普通點(diǎn)和有色點(diǎn),以及相反的情形時(shí)鹤树,可能會(huì)得到不一樣的結(jié)果铣焊。前一種比較會(huì)忽略顏色信息,而后一種比較總是返回false罕伯,因?yàn)閰?shù)類(lèi)型不正確曲伊,為了直觀的說(shuō)明問(wèn)題所在,我們創(chuàng)建一個(gè)普通點(diǎn)和一個(gè)有色點(diǎn)。
Point p1 = new Point(1, 2);
ColorPoint p2 = new ColorPoint(1, 2, Color.BLACK);
System.out.println(p1.equals(p2)); // true
System.out.println(p2.equals(p1)); // false
然而坟募,p1.equals(p2)返回true岛蚤,p2.equals(p1)返回false,也可以嘗試修正這個(gè)問(wèn)題懈糯,讓ColorPoint.equals在進(jìn)行"混合比較"式忽略顏色涤妒。
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Point)) {
return false;
}
if (!(obj instanceof ColorPoint)) {
return obj.equals(this);
}
ColorPoint colorPoint = (ColorPoint) obj;
return super.equals(obj) && color == colorPoint.color;
}
這種方式雖然提供了對(duì)稱(chēng)性,但是卻犧牲了傳遞性赚哗。例如:
ColorPoint p1 = new ColorPoint(1, 2, Color.BLACK);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.RED);
System.out.println(p1.equals(p2)); // true
System.out.println(p2.equals(p3)); // true
System.out.println(p3.equals(p1)); // false
以上例子很明顯這違反了傳遞性她紫,前兩種不需要比較顏色信息,而第三種比較則需要考慮顏色信息屿储。
此外贿讹,這種方法還可能導(dǎo)致無(wú)線遞歸的問(wèn)題,假設(shè)Point有兩個(gè)子類(lèi)够掠,如ColorPoint和SmellPoint民褂,它們各自都帶有這種equals方法,那么ColorPoint.equals(SmellPoint)的調(diào)用就會(huì)出現(xiàn)遞歸疯潭,將會(huì)拋出StackOverflowError
異常:舉個(gè)例子:
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Point)) {
return false;
}
if (!(obj instanceof ColorPoint)) {
return obj.equals(this);
}
ColorPoint colorPoint = (ColorPoint) obj;
return super.equals(obj) && color == colorPoint.color;
}
}
public class SmellPoint extends Point {
private final Color color;
public SmellPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Point)) {
return false;
}
if (!(obj instanceof SmellPoint)) {
return obj.equals(this);
}
return super.equals(obj) && ((SmellPoint) obj).color == this.color;
}
}
public class Main{
public static void main(String[] args) {
ColorPoint p1 = new ColorPoint(1, 2, Color.BLACK);
SmellPoint p2 = new SmellPoint(1, 2, Color.YELLOW);
System.out.println(p1.equals(p2));
}
}
console:
Exception in thread "main" java.lang.StackOverflowError
...
那么怎么解決呢助赞?事實(shí)上,這是面向?qū)ο笳Z(yǔ)言中關(guān)于等價(jià)關(guān)系的一個(gè)基本問(wèn)題袁勺,我們*無(wú)法*在擴(kuò)展可實(shí)例化的類(lèi)的同時(shí),即增加新的值組件畜普,同時(shí)又保留equals約定
期丰,除非愿意放棄面向?qū)ο蟮某橄笏鶐?lái)的優(yōu)勢(shì)。
你可能聽(tīng)說(shuō)過(guò)吃挑,在equals方法中用getClass測(cè)試代替instanceof測(cè)試钝荡,可以擴(kuò)展可實(shí)例化的類(lèi)和增加新的值組件,同時(shí)保留約定舶衬。
@Override
public boolean equals(Object obj) {
if (obj == null || obj.getClass() != getClass()) {
return false;
}
Point point = (Point) obj;
return point.x == x && point.y == y;
}
這段程序只有當(dāng)對(duì)象具有相同的實(shí)現(xiàn)類(lèi)時(shí)埠通,才能是對(duì)象相同,但是Point子類(lèi)的實(shí)例任然是一個(gè)Point逛犹,他仍然需要發(fā)揮作用端辱,但是如果采取這種方式,它就無(wú)法完成任務(wù)虽画!
假設(shè)我們要編寫(xiě)這樣一個(gè)方法舞蔽,以檢驗(yàn)?zāi)硞€(gè)點(diǎn)是否存處在單位園中,下面是可以采用的一種方式:
public class CounterPoint extends Point {
private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger();
public CounterPoint(int x, int y) {
super(x, y);
ATOMIC_INTEGER.incrementAndGet();
}
public static int numberCreated() {
return ATOMIC_INTEGER.get();
}
}
public class Main {
private static final Set<Point> unitCircle = new HashSet<>(Arrays.asList(new Point(1, 2)
, new Point(1, 0),
new Point(-1, 2)));
public static boolean onUnitCircle(Point p) {
return unitCircle.contains(p);
}
}
里氏替換原則認(rèn)為码撰,一個(gè)類(lèi)型的任何重要屬性也將適用于它的子類(lèi)型渗柿,因此為該類(lèi)型編寫(xiě)的任何方法,在它的子類(lèi)型上也應(yīng)該同樣運(yùn)行的很好脖岛。針對(duì)上述Point的子類(lèi)(如CountPoint)仍是Point朵栖,并且必須發(fā)揮作用的例子颊亮,這就是它的正式語(yǔ)句。但是假設(shè)我們將CountPoint實(shí)例傳給了onUnitCircle方法陨溅,如果Point類(lèi)使用了基于getClass的equals方法终惑,無(wú)論CountPoint實(shí)例的x和y值是什么,onUnitCircle都會(huì)返回false声登。這是因?yàn)橄駉nUnitCircle方法所用的HashSet這樣的集合狠鸳,利用equals方法檢驗(yàn)包含條件,沒(méi)有任何CountPoint實(shí)例與任何Point對(duì)應(yīng)悯嗓。
CounterPoint counterPoint = new CounterPoint(1, 2);
CounterPoint counterPoint1 = new CounterPoint(1, 0);
CounterPoint counterPoint2 = new CounterPoint(-1, 2);
System.out.println(onUnitCircle(counterPoint)); // false
System.out.println(onUnitCircle(counterPoint1)); // false
System.out.println(onUnitCircle(counterPoint2)); // false
但是件舵,如果在Point上使用基于instanceof的equals方法,當(dāng)遇到CountPoint時(shí)脯厨,相同的onUnitCircle方法就會(huì)工作得很好铅祸。
雖然沒(méi)有一種令人滿(mǎn)意的方式可以即擴(kuò)展可實(shí)例得類(lèi),又增加值組件合武,但是還有一種不錯(cuò)得權(quán)宜之計(jì):遵從第18條"復(fù)用優(yōu)先于繼承"得建議临梗。我們不再讓ColorPoint擴(kuò)展Point,而是在ColorPoint中加入一個(gè)私有Point域稼跳,以及一個(gè)公有的視圖方法盟庞,此方法返回一個(gè)與該有色點(diǎn)處在相同位置的普通Point對(duì)象:
public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color) {
this.point = new Point(x, y);
this.color = color;
}
public Point asPoint() {
return point;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ColorPoint)) {
return false;
}
ColorPoint c = (ColorPoint) obj;
return c.point.equals(point) && c.color.equals(color);
}
}
注意,你可以在一個(gè)抽象類(lèi)的子類(lèi)中增加值組件且不違反equals約定汤善,根據(jù)第23條的建議而得到的那種類(lèi)層次結(jié)構(gòu)來(lái)說(shuō)什猖,這一點(diǎn)非常重要。例如红淡,你可能有一個(gè)抽象類(lèi)Shape不狮,它沒(méi)有任何值組件,Circle子類(lèi)添加了一個(gè)radius域在旱,Rectangle子類(lèi)添加了length和width域摇零。只要不可能直接創(chuàng)建超類(lèi)的實(shí)例
,前面所述的種種問(wèn)題就都不會(huì)發(fā)生桶蝎。就保證了傳遞性驻仅。
一致性
:equals約定的第四個(gè)要求是,如果兩個(gè)對(duì)象相等登渣,它們就必須始終相等雾家,除非它們中有一個(gè)對(duì)象(或者兩個(gè)都)被修改。可變對(duì)象在不同的時(shí)候可以與不同的對(duì)象相等
绍豁,而不可變對(duì)象則不會(huì)這樣芯咧。當(dāng)在編寫(xiě)一個(gè)類(lèi)的時(shí)候,應(yīng)該仔細(xì)考慮它是否應(yīng)該是不可變的,如果認(rèn)為它應(yīng)該是不可變的敬飒,就必須保證equals方法滿(mǎn)足這樣的限制邪铲,相等的對(duì)象永遠(yuǎn)相等,不相等的對(duì)象永遠(yuǎn)不相等无拗。
無(wú)論類(lèi)是否不可變带到,都不要使equals方法依賴(lài)于不可靠的資源
。如果違反了這條
禁令英染,想要滿(mǎn)足一致性的要求就非常困難揽惹。
非空性
:最后一個(gè)要求沒(méi)有正式的名字,我們姑且稱(chēng)它為“非空性“四康,意思是指所有的對(duì)象都不能等于null搪搏,盡管很難想象在什么情況下o.equals(null)調(diào)用會(huì)意外的返回true,但是意外的拋出NullPointerException異常的情形卻不難想象闪金。通用約定允許拋出NullPointerException異常疯溺。許多類(lèi)的equals方法都通過(guò)一個(gè)顯式的null測(cè)試來(lái)防止這種情況;
@Override
public boolean equals(Object o){
if(o == null){
return false;
}
....
}
這項(xiàng)測(cè)試是不必要的哎垦,為了測(cè)試其參數(shù)的等同性囱嫩,equals方法必須先把參數(shù)轉(zhuǎn)換成適當(dāng)?shù)念?lèi)型,以便可以調(diào)用它的訪問(wèn)方法漏设,或者訪問(wèn)它的域
墨闲,在進(jìn)行轉(zhuǎn)換之前,equals方法必須使用instanceof操作符郑口,檢查其參數(shù)的類(lèi)型是否正確鸳碧。
@Override
public boolean equals(Object o){
if(!(o instanceof MyType)){
return false;
}
MyType type = (MyType) o;
....
}
如果漏掉了這一步類(lèi)型檢查,并且傳遞給equals方法參數(shù)又是錯(cuò)誤的類(lèi)型潘酗,那么equals方法將會(huì)拋出ClassCastException異常,這就違反了equals約定雁仲。但是仔夺,如果
instanceof的第一個(gè)操作數(shù)為null,那么攒砖,不管第二個(gè)是哪種類(lèi)型缸兔,instanceof操作符都指定應(yīng)該返回false,因此吹艇,如果把null傳給equals方法惰蜜,類(lèi)型檢查就會(huì)返回false,所以不需要顯式的null檢查受神。
結(jié)合所有要求抛猖,得出了以下實(shí)現(xiàn)高質(zhì)量equals方法的訣竅:
1.使用==操作符檢查”參數(shù)是否為這個(gè)對(duì)象的引用“
。如果是,則返回true财著,這只不過(guò)是一種性能優(yōu)化联四,如果比較操作有可能很昂貴,就值得這么做撑教。
2.使用instanceof操作符檢查”參數(shù)是否為正確的類(lèi)型“
朝墩,如果不是,則返回false伟姐。一般來(lái)說(shuō)收苏,所謂“正確的類(lèi)型”是指equals方法所在的那個(gè)類(lèi)。某些情況下愤兵,是指該類(lèi)所實(shí)現(xiàn)的某個(gè)接口鹿霸。如果類(lèi)實(shí)現(xiàn)的接口進(jìn)行了equals約定,允許在實(shí)現(xiàn)了該接口的類(lèi)之間進(jìn)行比較恐似,那就使用接口杜跷,集合接口如Set,List矫夷,Map和Map.Entry具有這樣的特性葛闷。
3.把參數(shù)轉(zhuǎn)換正確的類(lèi)型
,因?yàn)檗D(zhuǎn)換之前進(jìn)行過(guò)instanceof測(cè)試双藕,所以確保會(huì)成功淑趾。
4.對(duì)于該類(lèi)的每個(gè)關(guān)鍵域,檢查參數(shù)中的域是否與該對(duì)象中的對(duì)應(yīng)的域相匹配
忧陪。如果這些測(cè)試全部成功扣泊,則返回true,否則返回false嘶摊,如果第2步中的類(lèi)型是個(gè)接口延蟹,就必須接口方法訪問(wèn)參數(shù)中的域,如果該類(lèi)型是一個(gè)類(lèi)叶堆,也許就能夠直接訪問(wèn)參數(shù)中的域阱飘,這要取決與它們的可訪問(wèn)性。
對(duì)于既不是float也不是double類(lèi)型的基本類(lèi)型域虱颗,可以直接使用==操作符進(jìn)行比較沥匈,對(duì)于對(duì)象域,可以遞歸調(diào)用equals方法忘渔,對(duì)于float域高帖,可以使用靜態(tài)Float.compare(float f1,float f2)方法;對(duì)于double域畦粮,則使用Double.compare(double d1,double d2)散址。對(duì)float和double域進(jìn)行特殊的處理是有必要的乖阵,因?yàn)榇嬖谥鳩loat.NaN、-0.0f以及類(lèi)似的double常量爪飘;雖然可以用靜態(tài)方法Float.equals和Double.equals對(duì)float和double域進(jìn)行比較义起,但是每次比較都要進(jìn)行自動(dòng)裝箱,這回導(dǎo)致性能下降师崎,對(duì)于數(shù)組域默终,則要把以上這些原則應(yīng)用到每一個(gè)元素,如果數(shù)組域中的每個(gè)元素都很重要犁罩,就可以使用其中一個(gè)Arrays.equals方法齐蔽。
有些對(duì)象引用域包含null可能是合法的,所以床估,為了避免可能導(dǎo)致NullPointerException異常含滴,則使用靜態(tài)方法Object.equals(Object o1,Object o2)來(lái)檢查這類(lèi)域的等同性。
域的比較順序可能會(huì)影響equals方法的性能丐巫,為了獲得最佳的性能谈况,應(yīng)該最先比較最有可能不一致的域,或者開(kāi)銷(xiāo)低的域
递胧,最理想的情況是兩個(gè)條件同時(shí)滿(mǎn)足的域碑韵,不應(yīng)該比較那些不屬于對(duì)象邏輯狀態(tài)的域,例如用于同步操作的Lock域缎脾,也不需要比較衍生域祝闻,因?yàn)檫@些域可以由關(guān)鍵域計(jì)算獲得
,但是這樣做有可能提高equals方法的性能遗菠,如果衍生域代表了整個(gè)對(duì)象的綜合描述联喘,比較這個(gè)域可以節(jié)省在比較失敗時(shí)去比較實(shí)際數(shù)據(jù)所需要的開(kāi)銷(xiāo)
,例如:假設(shè)有一個(gè)Polygon類(lèi)辙纬,并緩存了該面積豁遭,如果兩個(gè)多邊形有著不同的面積,就沒(méi)有必要再去比較它們的邊和頂點(diǎn)贺拣。
在編寫(xiě)完equals方法之后蓖谢,應(yīng)該問(wèn)自己三個(gè)問(wèn)題:它是否對(duì)稱(chēng)性?它是否傳遞性纵柿?它是否一致性蜈抓?
并且不要只是自問(wèn)启绰,還要編寫(xiě)單元測(cè)試來(lái)檢驗(yàn)這些特性昂儒,除非用AutoValue(Goole開(kāi)源的框架)生成equals方法,在這種情況下就可以放心的省略測(cè)試委可。如果答案是否定渊跋,就要找出原因腊嗡,再相應(yīng)的修改equals方法的代碼邏輯。equals方法也必須滿(mǎn)足其他兩個(gè)特征(自反性和非空性)拾酝,當(dāng)然燕少,這兩種特性通常會(huì)自動(dòng)滿(mǎn)足。
根據(jù)上面的訣竅構(gòu)建equals方法的具體例子:
public final class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Person)) {
return false;
}
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
}
下面是最后一些告誡:
1.覆蓋equals方法時(shí)總是要覆蓋hashCode方法
2.不要企圖讓equals方法過(guò)于智能蒿囤,如果只是簡(jiǎn)單的測(cè)試域中的值是否相等客们,則不難做的equals約定,如果項(xiàng)過(guò)度的的去尋求各種等價(jià)關(guān)系材诽,則很容易陷入麻煩之中底挫。把任何一種別名形式考慮到等價(jià)范圍內(nèi),往往不是個(gè)好主意脸侥。例如建邓,F(xiàn)ile類(lèi)不應(yīng)該試圖把指向同一文件的符號(hào)鏈接當(dāng)作相等對(duì)象來(lái)看待
。所幸File類(lèi)沒(méi)有這樣做睁枕。
3.不要將equals聲明中的Object對(duì)象替換為其他的類(lèi)型官边。程序員編寫(xiě)出下面這樣的equals方法并不鮮見(jiàn),這會(huì)使得程序員花上好幾個(gè)小時(shí)都搞不清楚為什么它不能正常工作外遇。
public boolean equals(MyClass o){
....
}
這個(gè)方法并沒(méi)有覆蓋Object.equals方法注簿,因?yàn)閰?shù)應(yīng)該是Object類(lèi)型,相反臀规,它重載了Object.equals方法滩援,在正常的equals方法的基礎(chǔ)上,再提供一個(gè)強(qiáng)類(lèi)型的equals方法塔嬉,這樣會(huì)導(dǎo)致子類(lèi)中的Override注解產(chǎn)生錯(cuò)誤玩徊。
@Override注解的用法一致,就如本條目中所示谨究,可以防止犯這種錯(cuò)誤恩袱,這和equals方法不能編譯,錯(cuò)誤信息會(huì)告訴你到底哪里出了問(wèn)題胶哲。
總而言之畔塔,不要輕易的覆蓋equals方法,除非特殊需求鸯屿,在許多情況下澈吨,從Object處繼承就夠了。如果要覆蓋equals方法寄摆,一定要比較這個(gè)類(lèi)的所有關(guān)鍵域谅辣,并且檢查覆蓋的equals方法是否遵守equals合約的所有五個(gè)條款。