最近在閱讀《Effective Java》第3章裏讀到了關於 equals() 和 hashcode() 的一些介紹,這兩個方法是很多Java程序員容易弄混的,因此本文針對這兩個方法的用法和具體實現來做一些介紹。
equals() 與 hashcode() 的用處?
我們一般用equals()
來比較兩個對象的邏輯意義
上的值是否相同。舉個例子:
class Person {
String name;
int age;
long id;
}
我們現在有兩個Person的對象,person1 和person2,那麼什麼時候這兩個是相等的呢?對於兩個人而言,我們認爲如果他們倆名字、年齡和ID都完全一樣,那麼就是同一個人。也就是說,如果
person1.name = person2.name
person1.age = person2.age
person1.id = person2.id
那麼我們就認爲 person1.equals(person2)=true
。這就是表示equals是指二者邏輯意義上相等即可。
而 hashcode() 則是對一個對象進行hash計算得到的一個散列值,它有以下特點:
- 對象x和y的hashcode相同,不代表兩個對象就相同(x.equals(y)=true),可能存在hash碰撞;不過hashcode如果不相同,那麼一定是兩個不同的對象
- 如果兩個對象的equals()相等,那麼hashcode一定相等。
所以我們一般可以用hashcode來快速比較兩個對象互異
,因爲如果x.hashcode() != y.hashcode()
,那麼x.equals(y)=false
。
equals() 的特性
很多時候我們想要重寫某個自定義object的equals()方法,那麼一定要記住,你的equals()方法必須滿足下面四個條件:
- 自反性:對於非null的對象x,必須有
x.equals(x)=true
; - 對稱性:如果
x.equals(y)=true
,那麼y.equals(x)
必須也爲true
; - 傳遞性:如果
x.equals(y)=true
而且y.equals(z)=true
,那麼x.equals(z)
必須爲true
; - 對於非null的對象x,一定有
x.equals(null)=false
如何重寫 equals() 方法呢?
一般而言,如果你要重寫 equals() 方法,有下面一套模版代碼可以參考:
- 首先使用
==
來判斷兩個對象是否引用相同
; - 使用
instanceof
來判斷兩個對象是否類型相同
; - 如果類型相同,則把待比較參數轉型;
- 比較兩個對象內部每個邏輯值是否相等,只有全部相等才返回true,或者返回false;
- 測試這個方法是否能滿足上面幾個特性。
Java 源碼 String 裏 equals() 和 hashcode() 實現
看完上面的特性和重寫方法你可能有點頭大,下面我們來看一下Java裏的 String 是如何實現的吧,是否滿足上面幾個特性呢。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
可以看到,上面的方法依次執行了下面的步驟:
- 比較引用
this == anObject
; - 判斷類型
anObject instanceof String
; - 轉型
String anotherString = (String)anObject
; - 比較邏輯值 對 String 而言,首先要
length
相等n == anotherString.value.length
;然後要每一個字符相等,見代碼,最後返回結果。
下面我寫了一段測試代碼來驗證是否符合上面幾點特性:
private static void testStringEquals() {
String x = "First";
String y = "First";
String z = new String("First");
System.out.println(x.equals(x));
System.out.println((x.equals(y) && y.equals(x)));
if (x.equals(y) && y.equals(x)) {
System.out.println(x.equals(z));
}
System.out.println(x.equals(null));
}
打印結果如下:
true
true
true
false
說明是符合的。
然後我們再看下 hashcode() 的源代碼實現,我們知道,hashcode的含義是計算hash散列值,其實就是對一個對象快速計算一個散列值,用來判異
使用:只要 hashcode() 不同,那麼兩個對象一定不同。下面我們看下 String 是如何計算自己的hash值的。
private final char value[]; /** The value is used for character storage. */
private int hash; /** Cache the hash code for the string Default to 0 */
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
其中用來計算 hashcode 主要是這段代碼
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
其中,value是內部存儲string值的字符數組。計算hashcode的方法就是依次遍歷每一個字符,乘以31後再加上下一個字符。例如"a"的hashcode就是 97;"aa"的hashcode是 31*97+97
=3104。因此可以看出,hashcode不同的兩個 String 對象一定不是同一個對象
。
謹記:重寫 equals() 時要保證:兩個equal的對象一定有相同的hashcode
很多人在重寫 equals() 時忽視了這一點,沒有保證兩個equal的對象具備相同的hashcode,從而導致了奇怪的錯誤。
下面舉一個例子,我先只重寫 PhoneNumberWithoutHashcode
的 equals() 方法:
class PhoneNumberWithoutHashcode {
final short countryCode;
final short number;
public PhoneNumberWithoutHashcode(int countryCode, int number) {
this.countryCode = (short) countryCode;
this.number = (short) number;
}
@Override
public boolean equals(Object obj) {
// 1. check == reference
if(obj == this) {
return true;
}
// 2. check obj instance
if (!(obj instanceof PhoneNumberWithoutHashcode))
return false;
// 3. compare logic value
PhoneNumberWithoutHashcode anObj = (PhoneNumberWithoutHashcode) obj;
return anObj.countryCode == this.countryCode
&& anObj.number == this.number;
}
}
下面我們來創建兩個相同的對象,看看它們的 equals() hashcode() 返回值如何。
private static void test() {
PhoneNumberWithoutHashcode p1 = new PhoneNumberWithoutHashcode(86, 123123);
PhoneNumberWithoutHashcode p2 = new PhoneNumberWithoutHashcode(86, 123123);
System.out.println("p1.equals(p2)=" + p1.equals(p2));
System.out.println("p1.hashcode()=" + p1.hashCode());
System.out.println("p2.hashcode()=" + p2.hashCode());
}
可以得到結果如下:
p1.equals(p2)=true
p1.hashcode()=1846274136
p2.hashcode()=1639705018
可以看出,二者是 equals 的,但是hashcode不一樣。這違背了 Java準則,會導致什麼結果呢?
private static void test() {
PhoneNumberWithoutHashcode p1 = new PhoneNumberWithoutHashcode(86, 123123);
PhoneNumberWithoutHashcode p2 = new PhoneNumberWithoutHashcode(86, 123123);
System.out.println("p1.equals(p2)=" + p1.equals(p2));
HashMap<PhoneNumberWithoutHashcode, String> map = new HashMap<>();
map.put(p1, "TheValue");
System.out.println("Result: " + map.get(p2));
}
讀者覺得會打印什麼呢?Result: TheValue
嗎?我們來看下運行結果:
p1.equals(p2)=true
Result: null
問題來了,p1和p2是equal的,但是確不是同樣的key,至少對於HashMap而言,它們倆不是同一個key,爲什麼呢?
我們看一下 HashMap 是怎麼put和get的吧。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
從這段代碼可以看到,p1 和 p2 被存儲時就計算了一次 hash(key)
,如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
其實就是調用了 key.hashCode()
方法,而我們知道雖然 p1.equals(p2)=true
,但是p1.hashCode() != p2.hashCode()
,因此 p1 和 p2 對 HashMap 而言壓根就是兩個 key,當然互相取不到對方的 value了。
那麼要如何改進這個類呢?我們再來實現它的 hashcode 方法吧。
class PhoneNumber {
protected final short countryCode;
protected final short number;
public PhoneNumber(int countryCode, int number) {
this.countryCode = (short) countryCode;
this.number = (short) number;
}
@Override
public boolean equals(Object obj) {
// 1. check == reference
if (this == obj)
return true;
// 2. check obj instance
if (!(obj instanceof PhoneNumber))
return false;
// 3. compare logic value
PhoneNumber target = (PhoneNumber) obj;
return target.number == this.number
&& target.countryCode == this.countryCode;
}
@Override
public int hashCode() {
return (31 * this.countryCode) + this.number;
}
}
這時我們的測試代碼:
private static void test() {
PhoneNumber p1 = new PhoneNumber(86, 12);
PhoneNumber p2 = new PhoneNumber(86, 12);
System.out.println("p1.equals(p2)=" + p1.equals(p2));
System.out.println("p1.hashcode()=" + p1.hashCode());
System.out.println("p2.hashcode()=" + p2.hashCode());
HashMap<PhoneNumber, String> map = new HashMap<>(2);
map.put(p1, "TheValue");
System.out.println("Result: " + map.get(p2));
}
打印結果如下:
p1.equals(p2)=true
p1.hashcode()=88076
p2.hashcode()=88076
Result: TheValue
說明重寫hashcode後就能保證 PhoneNumber
在 HashMap
里正常運行了,畢竟像這種 HashMap HashSet 之類的都要基於對象的hash值。
小結
如果存在遺漏錯誤歡迎讀者提出,謝謝。
wingjay