譯自 http://www.javaworld.com/article/2072762/java-app-dev/object-equality.html
每一個Java對象都從java.lang.Object
繼承了1些方法:
Creational methods | |
---|---|
Object() | Default no-argument constructor |
clone() | Returns a new instance of the class |
**Equality methods** | |
equals(Object) | Returns true if this instance is equals to the argument |
hashCode() | Returns a hash code based on the instance data |
**Synchronizing methods** | |
notify() | Sends a signal to a waiting thread (on the current instance) |
notifyAll() | Sends a signal to all waiting threads (on the current instance) |
wait() | Forces the current thread to wait for a signal (on the current instance) |
**Other methods** | |
toString() | Returns a string representation of the object |
finalize() | Perform garbage-collection duties |
getClass() | Returns the Class object associated with the instance |
這些方法都提供了默許實現,其中除notify(), notifyAll(), wait()
3個方法是final
的,沒法被子類重寫,其他方法都可以被重寫。這篇文章將討論如何重寫equals()
和hashCode()
方法。
equals()
方法的目的是判斷參數對象和當前實例是不是相等。實際上,java.util
包中的所有集合類都使用了該方法,還有其他很多較為底層的庫(如RMI,JDBC,等等)都隱式地依賴于該方法的正確性。如果兩個對象被認為是相等的,那末該方法返回tree
,否則返回false
。哪些內容相等才被認為是兩個對象相等由每一個類自己定義。
由于計算對象是不是相等是1件很耗時的事,Java提供了1種快速判斷兩個對象是不是相等的方法,即hashCode()
。該方法根據對象的內部數據結構生成1個小的數值,被稱為哈希碼(hash code),如果兩個對象具有不同的哈希碼,那末他們不可能相等。(比如字典里的兩個英文單詞,如果它們都以A開頭,那末它們有可能相等,如果1個以A開頭,1個以B開頭,那末它們不可能相等。)
計算哈希碼的目的是哈希要比計算全部對象的相等性快。HashMap
就使用了哈希碼來盡量地避免計算對象的相等性,HashMap
比List
快的1個緣由就是,List
需要搜索全部數據結構判斷對象是不是存在,而HashMap
只需搜索那些具有相同哈希值的對象。
切記,1個類只重寫equals()
方法而不重寫hashCode()
方法是毛病的。在繼承體系中,只需父類提供1個hashCode()
方法便可,后面會詳細討論。
方法簽名必須為
public boolean equals(Object obj)
`</pre>
注意:任何類的`equals()`方法的參數都必須是`Object`類型,否則該方法就不是重寫,而是重載了,當判斷兩個對象是不是相等時就會調用`java.lang.Object`類的默許的`equals()`方法,而非你定義的。
Javadoc中描寫`equals()`方法必須滿足:
a.equals(b)
,那末b.equals(a)
;a.equals(b)
,并且b.equals(c)
,那末a.equals(c)
;非null,1個對象任什么時候候都不能等于null
,即a.equals(null)
永久返回false。
根據上面規則,很容易寫出1個equals()
方法的實現,只需要比較以下內容:
this
,返回true
;(自反性)null
,返回false
;(非null)false
;(對稱性)static
和非transient
域都是相等的。(對稱性,傳遞性)為什么不需要比較static
域和transient
域?由于static
數據是屬于類的而非對象實例,所有對象實例同享static
數據。transient
關鍵字的目的是對象在序列化時不讓某些域寫入(如為安全起見,用戶的銀行卡號和密碼等信息,不希望在網絡操作中被傳輸,那末對這些變量加上transient關鍵字后就不會被持久化 [詳見]),如果這些域用于測試對象是不是相等,那末同1個對象在序列化之前和以后就會不等。
以2維平面坐標中的點Point
為例,我們實現1個簡單的equals()
方法:
`public class Point { private static double version = 1.0; private transient double distance; private int x, y;public boolean equals(Object other) { if (other == this) return true; if (other == null) return false; if (getClass() != other.getClass()) return false; Point point = (Point)other; return (x == point.x && y = point.y); }
}
`
注意:這里使用getClass()
來比較兩個對象是不是屬于同1個類型,而非instancof
,后面我們會討論為什么不用instanceof
。
如果1個對象里含有援用類型,那末如何比較兩個對象的援用是不是相等?答案是根據以下規則:
this
的援用變量為null
,那末other
相應的援用變量也必須為null
;this
的援用變量不為null
,那末它必須和other
相應的援用變量equals()
。以下面的Person
類,含有兩個援用類型的變量name和birth:
`public class Person { private String name; private Date birth;public boolean equals(Object other) { if (other == this) return true; if (other == null) return false; if (getClass() != other.getClass()) return false; Person person = (Person)other; return (name == person.name || (name != null && name.equals(person.name))) && (birth == person.birth || (birth != null && birth.equals(person.birth))); }
}
`
注意:name == person.name
檢查了兩個援用都為null
的情況和兩個援用指向同1個對象的情況。在調用name.equals(person.name)
方法前1定要先檢查name != null
,否則將會拋出’NullPointerException’。
哈希碼(hash code)就是根據實例數據計算出來的1個int
值,如果兩個實例被認為是equals
,那末它們必須具有相同的hash code。因此,hash code**只能**根據equals()
方法中所比較的變量域來計算,但其實不1定需要使用所有的域。
2維平面上的點Point
的hashCode()
實現以下:
`public class Point { private int x, y;public boolean equals(Object other) { // see above } publice int hashCode() { return x; }
}
`
上面的hashCode()
實現時正確的(雖然不是最優的),由于它依賴于equals()
方法的比較域x
。
固然,我們期望不同對象的hash code越分散越好。上面的實現中,所有具有相同x
坐標的點都具有相同的hash code,我們可以通過以下方法改進:
使用多個變量的乘積;
^
;給整數變量乘上1個質數。
`public class Point {
private int x, y;
public boolean equals(Object other) {
// see above
}
publice int hashCode() {
return 31*x ^ 37*y;
}
}
`
但是,為了使hash code的計算速度快,1般不使用乘法。
如果1個類包括了援用變量,那末就能夠使用援用變量的hashCode()
方法。但是,跟equals()
方法1樣,1定要注意援用是不是為null
,否則可能拋出NullPointerException
異常。對null
的情況,可以返回1個固定值(這個固定值應當是正整數,由于在1些hash map的實現中這些值可能有特殊的意義)。
`public class Person { private String name; private Date birht;public boolean equals(Object other) { // see above } public int hashCode() { return (name == null ? 17 : name.hashCode()) ^ (birht == null ? 31 : birth.hashCode()); }
}
`
這兩個方法的默許實現只適用于簡單的情況。equals
方法只有在與其本身比較時才返回true
。hashCode()
方法依賴于單1對象的哈希(unique instance hash,如對象在內存中的地址或不同虛擬機有不同的實現)。
由于hashCode()
依賴于對象的唯1性(identity),所以只重寫equals()
方法而不重寫hashCode()
方法是毛病的。否則,兩個對象多是相等的,卻有不同的哈希值。如果實在沒有適合的哈希值計算方法,那末可以返回1個常數(比如7),這樣也比使用默許的實現好,雖然這樣會致使Map
退化成List
。
equals()
和hashCode()
必須都盡量地快,由于它們常常被重復地調用。
equals()
方法的某些部份要比其他部份快,所以可以先比較快的部份。比如,基本數據類型的比較要快于援用類型的equals()
,那末可以先比較基本數據類型。一樣地,如果兩個對象類型不同,那末就沒必要比較任何數據域了。
如果1個對象是只讀的(immutable),那末可以提早計算出它的哈希值。當這樣的對象被創建時,所有的值都會通過構造函數傳入,這時候就能夠計算出它的哈希值。
`public class Point { private final int x, y; private final int hashCode;public Point(int x, int y) { this.x = x; this.y = y; this.hashCode = 31*x ^ 37*y; } public boolean equals(Object other) { // see above } public int hashCode() { return hashCode; }
}
`
注意,上面代碼中的變量x和y都是final
的,保證以后不會被改變,所以可以在創建對象時就計算出哈希值。
在Joshua Bloch非常著名的《Effective Java》1書中,他推薦使用instanceof
來測試以決定對象的類型,表面上看這是1個很好的想法,實際上這有1個重大缺點,由于instanceof
不是對稱的。
下面是Bloch推薦的做法:
`public class BadPoint { private int x, y;public boolean equals(Object other) { if (other == this) return true; if (!(other instanceof BadPoint)) return false; // Bad!!! BadPoint point = (BadPoint)other; return (x == point.x && y == point.y); } public int hashCode() { return x + y; }
}
`
由于這個代碼更短,而且是《Effective Java》1書所推薦,所以已根深蒂固于很多Java程序員的編碼中。這同樣成為本書最具爭議的內容之1。
使用instanceof
的最大問題是它不具有對稱性,當使用繼承時這個問題將會體現出來:
`public class BadPoint3D extends BadPoint { private int z;public boolean equals(Object other) { if (!super.equals(other)) return false; if (!(other instanceof BadPoint3D)) return false; // Bad!!! BadPoint3D point = (BadPoint3D)other; return (z == point.z); }
}
`
當1個BadPoint
對象和1個BadPoint3D
對象比較時就會出現問題,badPoint instanceof badPoint3D == false
,而point3D instanceof badPoint == true
。
Point
`public class Point { private static double version = 1.0; private transient double distance;private String name; private int x, y; public Point(int x, int y) { this.x = x; this.y = y; } public Point(String name, int x, int y) { this(x, y); this.name = name; } public boolean equals(Object other) { if (other == this) return true; if (other == null) return false; if (getClass() != other.getClass()) return false; Point point = (Point)other; return (x == point.x && y == point.y && (name = point.name || (name != null && name.equals(point.name)))); } public int hashCode() { return x ^ y; }
}
`
Point3D
`public class Point3D { private int z;public Point3D(int x, int y, int z) { super(x, y); this.z = z; } public Point3D(String name, int x, int y, int z) { super(name, x, y); this.z = z; } public boolean equals(Object other) { if (!super.equals(other)) return false; Point3D point = (Point3D)other; return (z == point.z); } public int hashCode() { return super.hashCode() ^ z; }
}
更深的學習可以參看 如何在Java中避免equals方法的隱藏圈套 | 酷 殼 - CoolShell.cn
說明:本文譯自javaword.com,如果侵權,請聯系站長刪除。同時,如果轉載本文,請注明本文鏈接。
上一篇 ubuntu安裝配置JDK
下一篇 [XML]學習筆記(九)DOM