總結對==、equals、hashCode的認識
Java
先說一下為什麼突然想寫關於三者的文章。
在最近一個項目的開發中,需要對前端傳來的對象列表進行去重。
使用HashSet是一個比較好的辦法:
//為了保證順序,使用了LinkedHashSet
LinkedHashSet<User> set = new LinkedHashSet<User>(users.size());
set.addAll(users);
users.clear();
users.addAll(set);
按照設想,如果users有兩個內容相同的對象比如:
{
"users": [{
"id": 1,
"name": "leo"
}, {
"id": 1,
"name": "leo"
}]
}
經過LinkedHashSet的去重之後,應該只剩下一個id為1,name為leo的對象。
但實際情況卻是兩個都保留下來,去重沒有達到效果。
原因很容易推斷出來,在HashSet中認為兩個User不相同,也就是user1.equals(user2)應該返回true,卻返回了false。
之前的文章《從Integer(Long)的比較說開去》有提到,==比較對象時,比的是內存地址,真正想比較對象的內容,需要使用equals方法。
如果在User類中不重寫equals方法的話,會直接調用Object的equals方法:
public boolean equals(Object obj) {
return (this == obj);
}
直接比較兩個對象的內存地址,肯定返回false了!
這就決定了我們需要重寫equals方法。《Java面試題 Part17 手寫HashMap(二)》一文也有提及。
總結一下==與equals:
==比的是內存地址,它比的是二者是否是同一個——內存地址都一樣,肯定是同一個
equals比的內容是否相等。
好,說完了==與equals,該說說本文我最想說的hashCode。
在《Effective Java》中提到,重寫equals,也要重寫hashCode。
先想一下如果只重寫了equals,沒有重寫hashCode會是什麼樣?
還是拿之前的列表來比較
user1.equals(user2),結果為true
但是user1.hashCode()值是不等於user2.hashCode()
具體的源代碼就不貼了,大家可以自己驗證一下。
這個很好理解,沒有重寫,就使用Objec的hashCode方法(這是一個本地方法),因為兩個User的內存地址都不一樣,所以hashCode不一樣。
如果,我們在實際工作中,只是拿這個類進行equals,不重寫hashCode是沒問題的!
但是!如果像我之前遇到的問題,需要把對象放入散列表中(HashMap、HashSet、HashTable),就必須重寫hashCode()!
原因可以看《Java面試題 Part16 手寫HashMap(一)》一文,就是因為在put的時候,是先看兩個對象的hashCode,如果兩個對象內容相同,但是hashCode不同,在散列表中就認為是不同。
所以才有了以下的通常規則:
- 一個對象無論hashCode多少次,值都是相同的。
- 如果a.equals(b)==true,那麼a.hashCode()也必須等於b.hashCode()。
- 如果a.equals(b)==false,那麼不強求a.hashCode()也必須不等於b.hashCode()——即,兩個不相等的對象,hashCode是可以相同的。但是最好讓兩個不相等的對象生成不同的hashCode,這樣在散列表中,可以直接根據不同的hashCode分配,不需要再equals了,性能提高。
所以我上面遇到的問題,就需要重寫equals和hashCode方法。
重寫hashCode,大家看Java源代碼(比如String)和教程,都會提到一個乘數:31——這是一個質數(也叫素數)。
這就說說我寫本文的最終目的:為什麼是31?
我們重寫hashCode,就是為了讓不同對象散列的更厲害。
而拿一個數乘以一個質數,得到的結果更容易產生唯一性,散列值衝突的概率相對更小。
但是如果我們使用一個很大的質數,衝突的概率小了,但是產生的hashCode值就大了,大到可能超過Integer的最大值(hashCode值是int類型),溢出了!
如果使用是個很小的質數,倒是不溢出了,但是散列衝突的概率就變大了。
所以要選一個不溢出,散列衝突又不太嚴重可以忍受的質數,根據國外研究人員的測算,31,33,37,39,41是比較合適的質數。
而選用31,是因為N*31=(N<<5)-N,也就是說某個數N乘以31,可以轉化為N左移5位,再減去N。位移運算的效率是極高的。
當然,也有人提出使用37性能也很快,因為A=N*37可以轉化為兩個步驟:X=N+8N,A=N+4X,而這兩個步驟對應一個LEA X86指令,運算速度也是非常快。
所以大家在commons-lang包中的HashCodeBuilder類中可以看到默認的乘數就是37。
至於大家在各種文章上看到的result這個數的初始值,首先不能是0,
原因很簡單,result=result*37+value.hashCode(),如果result為0,乘以什麼都是0,散列衝突會很嚴重。
而有的文章將result設置為1,在HashCodeBuilder中,默認是17。
17也是質數,結合之前的說法「而拿一個數乘以一個質數,得到的結果更容易產生唯一性,散列值衝突的概率相對更小。」
大家可以自己品味一下,result=1和result=17二者的微妙之處。
![](https://pic.pimg.tw/zzuyanan/1488615166-1259157397.png)
![](https://pic.pimg.tw/zzuyanan/1482887990-2595557020.jpg)
TAG:Java個人學習心得 |