當前位置:
首頁 > 知識 > 總結對==、equals、hashCode的認識

總結對==、equals、hashCode的認識

總結對==、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不同,在散列表中就認為是不同。

所以才有了以下的通常規則:

  1. 一個對象無論hashCode多少次,值都是相同的。
  2. 如果a.equals(b)==true,那麼a.hashCode()也必須等於b.hashCode()。
  3. 如果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二者的微妙之處。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Java個人學習心得 的精彩文章:

從Integer(Long)的比較說開去

TAG:Java個人學習心得 |