深入equals方法
equals方法的重要性毋須多言,只要你想比較的兩個(gè)對(duì)象不愿是同一對(duì)象,你就應(yīng)該實(shí)現(xiàn)equals方法,讓對(duì)象用你認(rèn)為相等的條件來(lái)進(jìn)行比較。
下面的內(nèi)容只是API的規(guī)范,沒有什么太高深的意義,但我之所以最先把它列在這兒,是因?yàn)檫@些規(guī)范在事實(shí)中并不是真正能保證得到實(shí)現(xiàn)。
- 對(duì)于任何引用類型, o.equals(o) == true成立。
- 如果 o.equals(o1) == true 成立,那么o1.equals(o)==true也一定要成立。
- 如果 o.equals(o1) == true 成立且? o.equals(o2) == true 成立,那么o1.equals(o2) == true 也成立。
- 如果第一次調(diào)用o.equals(o1) == true成立再o和o1沒有改變的情況下以后的任何次調(diào)用都成立。
- o.equals(null) == true 任何時(shí)間都不成立。
以上幾條規(guī)則并不是最完整的表述,詳細(xì)的請(qǐng)參見API文檔。
對(duì)于Object類,它提供了一個(gè)最最嚴(yán)密的實(shí)現(xiàn),那就是只有是同一對(duì)象是,equals方法才返回true,也就是人們常說(shuō)的引用比較而不是值比較。這個(gè)實(shí)現(xiàn)嚴(yán)密得已經(jīng)沒有什么實(shí)際的意義,所以在具體子類(相對(duì)于Object來(lái)說(shuō))中,如果我們要進(jìn)行對(duì)象的值比較,就必須實(shí)現(xiàn)自己的equals方法。
先來(lái)看一下以下這段程序:
public boolean equals(Object obj)
{
if (obj == null) return false;
if (!(obj instanceof FieldPosition))
return false;
FieldPosition other = (FieldPosition) obj;
if (attribute == null) {
if (other.attribute != null) {
return false;
}
}
else if (!attribute.equals(other.attribute)) {
return false;
}
return (beginIndex == other.beginIndex
&& endIndex == other.endIndex
&& field == other.field);
}
這是JDK中java.text.FieldPosition的標(biāo)準(zhǔn)實(shí)現(xiàn),似乎沒有什么可說(shuō)的.?我相信大多數(shù)或絕大多數(shù)程序員認(rèn)為,這是正確的合法的equals實(shí)現(xiàn).畢竟它是JDK的API實(shí)現(xiàn)啊.?還是讓我們以事實(shí)來(lái)說(shuō)話吧:
package debug;
import java.text.*;
public class Test {
public static void main(String[] args) {
FieldPosition fp = new FieldPosition(10);
FieldPosition fp1 = new MyTest(10);
System.out.println(fp.equals(fp1));
System.out.println(fp1.equals(fp));
}
}
class MyTest extends FieldPosition{
int x = 10;
public MyTest(int x){
super(x);
this.x = x;
}
public boolean equals(Object o){
if(o==null) return false;
if(!(o instanceof MyTest )) return false;
return ((MyTest)o).x == this.x;
}
}
運(yùn)行一下看看會(huì)打印出什么:
System.out.println(fp.equals(fp1));打印true
System.out.println(fp1.equals(fp));打印flase
兩個(gè)對(duì)象,出現(xiàn)了不對(duì)稱的equals算法.問(wèn)題出在哪里(腦筋急轉(zhuǎn)彎:當(dāng)然出在JDK實(shí)現(xiàn)的BUG)?
我相信有太多的程序員(除了那些根本不知道實(shí)現(xiàn)equals方法的程序員外)在實(shí)現(xiàn)equals方法時(shí)都用過(guò)instanceof運(yùn)行符來(lái)進(jìn)行短路優(yōu)化的,實(shí)事求是地說(shuō)很長(zhǎng)一段時(shí)間我也這么用過(guò)。太多的教程,文檔都給了我們這樣的誤導(dǎo)。而有些稍有了解的程序員可能知道這樣的優(yōu)化可能有些不對(duì)但找不出問(wèn)題的關(guān)鍵。另外一種極端是知道這個(gè)技術(shù)缺陷的骨灰級(jí)專家就提議不要這樣應(yīng)用。
我們知道,"通常"要對(duì)兩個(gè)對(duì)象進(jìn)行比較,那么它們"應(yīng)該"是同一類型。所以首先利用nstanceof運(yùn)行符進(jìn)行短路優(yōu)化,如果被比較的對(duì)象不和當(dāng)前對(duì)象是同一類型則不用比較返回false,但事實(shí)上,"子類是父類的一個(gè)實(shí)例",所以如果 子類 o instanceof 父類,始終返回true,這時(shí)肯定不會(huì)發(fā)生短路優(yōu)化,下面的比較有可能出現(xiàn)多種情況,一種是不能造型成子類而拋出異常,另一種是父類的private 成員沒有被子類繼承而不能進(jìn)行比較,還有就是形成上面這種不對(duì)稱比較。可能會(huì)出現(xiàn)太多的情況。
那么,是不是就不能用 instanceof運(yùn)行符來(lái)進(jìn)行優(yōu)化?答案是否定的,JDK中仍然有很多實(shí)現(xiàn)是正確的,如果一個(gè)class是final的,明知它不可能有子類,為什么不用 instanceof來(lái)優(yōu)化呢?
為了維護(hù)SUN的開發(fā)小組的聲譽(yù),我不說(shuō)明哪個(gè)類中,但有一個(gè)小組成員在用這個(gè)方法優(yōu)化時(shí)在后加加上了加上了這樣的注釋:
if (this == obj) // quick check
return true;
if (!(obj instanceof XXXXClass)) // (1) same object?
return false;
可能是有些疑問(wèn),但不知道如何做(不知道為什么沒有打電話給我......)
那么對(duì)于非final類,如何進(jìn)行類型的quick check呢?
if(obj.getClass() != XXXClass.class) return false; 用被比較對(duì)象的class對(duì)象和當(dāng)前對(duì)象的class比較,看起來(lái)是沒有問(wèn)題,但是,如果這個(gè)類的子類沒有重新實(shí)現(xiàn)equals方法,那么子類在比較的時(shí)候,obj.getClass() 肯定不等于XXXCalss.class,也就是子類的equals將無(wú)效,所以if(obj.getClass() != this.getClass()) return false;才是正確的比較。
另外一個(gè)quick check是if(this==obj) return true;
是否equals方法一定比較的兩個(gè)對(duì)象就一定是要同一類型?上面我用了"通常",這也是絕大多數(shù)程序員的愿望,但是有些特殊的情況,我們可以進(jìn)行不同類型的比較,這并不違反規(guī)范。但這種特殊情況是非常罕見的,一個(gè)不恰當(dāng)?shù)睦邮牵琁nteger類的equals可以和Sort做比較,比較它們的value是不是同一數(shù)學(xué)值。(事實(shí)上JDK的API中并沒有這樣做,所以我才說(shuō)是不恰當(dāng)?shù)睦樱T谕瓿蓂uick check以后,我們就要真正實(shí)現(xiàn)你認(rèn)為的“相等”。對(duì)于如果實(shí)現(xiàn)對(duì)象相等,沒有太高的要求,比如你自己實(shí)現(xiàn)的“人”類,你可以認(rèn)為只要name相同即認(rèn)為它們是相等的,其它的sex,ago都可以不考慮。這是不完全實(shí)現(xiàn),但是如果是完全實(shí)現(xiàn),即要求所有的屬性都是相同的,那么如何實(shí)現(xiàn)equals方法?
class Human{
private String name;
private int ago;
private String sex;
....................
public boolean equals(Object obj){
quick check.......
Human other = (Human)ojb;
return this.name.equals(other.name)
&& this.ago == ohter.ago
&& this.sex.equals(other.sex);
}
}
這是一個(gè)完全實(shí)現(xiàn),但是,有時(shí)equals實(shí)現(xiàn)是在父類中實(shí)現(xiàn),而要求被子類繼承后equals能正確的工作,這時(shí)你并不事實(shí)知道子類到底擴(kuò)展了哪些屬性,所以用上面的方法無(wú)法使equals得到完全實(shí)現(xiàn)。
一個(gè)好的方法是利用反射來(lái)對(duì)equals進(jìn)行完全實(shí)現(xiàn):
public boolean equals(Object obj){
quick check.......
Class c = this.getClass();
Filed[] fds = c.getDeclaredFields();
for(Filed f:fds){
if(!f.get(this).equals(f.get(obj)))
return false;
}
return true;
}
為了說(shuō)明的方便,上明的實(shí)現(xiàn)省略了異常,這樣的實(shí)現(xiàn)放在父類中,可以保證你的子類的equals可以按你的愿望正確地工作。
關(guān)于equals方法的最后一點(diǎn)是:如果你要是自己重寫(正確說(shuō)應(yīng)該是履蓋)了equals方法,那同時(shí)就一定要重寫hashCode().為是規(guī)范,否則.............
我們還是看一下這個(gè)例子:
public final class PhoneNumber {
private final int areaCode;
private final int exchange;
private final int extension;
public PhoneNumber(int areaCode, int exchange, int extension) {
rangeCheck(areaCode, 999, "area code");
rangeCheck(exchange, 99999999, "exchange");
rangeCheck(extension, 9999, "extension");
this.areaCode = areaCode;
this.exchange = exchange;
this.extension = extension;
}
private static void rangeCheck(int arg, int max, String name) {
if(arg < 0 || arg > max)
throw new IllegalArgumentException(name + ": " + arg);
}
public boolean equals(Object o) {
if(o == this)
return true;
if(!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.extension == extension && pn.exchange == exchange && pn.areaCode == areaCode;
}
}
注意這個(gè)類是final的,所以這個(gè)equals實(shí)現(xiàn)沒有什么問(wèn)題。
我們來(lái)測(cè)試一下:
public static void main(String[] args) {
Map hm = new HashMap();
PhoneNumber pn = new PhoneNumber(123, 38942, 230);
hm.put(pn, "I love you");
PhoneNumber pn1 = new PhoneNumber(123, 38942, 230);
System.out.println(pn);
System.out.println("pn.equals(pn1) is " + pn.equals(pn1));
System.out.println(hm.get(pn1));
System.out.println(hm.get(pn));
}
既然pn.equals(pn1),那么我put(pn,"I love you");后,get(pn1)這什么是null呢?
答案是因?yàn)樗鼈兊膆ashCode不一樣,而hashMap就是以hashCode為主鍵的。
所以規(guī)范要求,如果兩個(gè)對(duì)象進(jìn)行equals比較時(shí)如果返回true,那么它們的hashcode要求返回相等的值。
好了,休息,休息一下。。。。。。。。。。。。。。。。
轉(zhuǎn)載自dev2dev網(wǎng)友a(bǔ)xman的go deep into java專欄。
個(gè)人自述
一個(gè)男人.
一個(gè)寫程序的男人.
一個(gè)寫程序并正在從程序中尋找快樂的男人.
一個(gè)寫程序并正在從程序中尋找快樂并把快樂傳遞給大家的男人.
一個(gè)書生.
一個(gè)寂寞的書生.
一個(gè)寂寞的梅香竹影下敲聲寫韻的書生.
一個(gè)寂寞的梅香竹影下敲聲寫韻晨鐘暮鼓中逸氣揚(yáng)劍的書生.
那個(gè)男人是位書生。沒有人知道他的姓名,居無(wú)定所,行無(wú)定蹤,亦耕變讀,或漁或樵。
所以有人叫他樵夫(Axman),有人叫他漁郎(fisher)。