JAVA中為什么要一起重寫equals和hashCode方法?
在Effective Java一書中,提到了一點:在每個覆蓋了equals方法的類中,都必須覆蓋hashCode方法。為什么要有這個要求呢?
這一要求與HashMap,HashTable等基于hash的集合的工作機制有關(guān),如果不同時提供equals和hashCode方法,可能會導(dǎo)致這類集合無法正常工作。
HashMap的工作機制
HashMap是我們常用的一個集合類,它可以映射鍵值對,非常方便。
在后臺,它把我們的鍵值對打包成一個Entry<key,value>,然后將這個Entry存起來。當使用鍵查找時,HashMap會在存放Entry的地方查找符合要求的鍵,然后返回存儲的值。
存放的地方是一個桶,以4個大小的桶為例,其存放方式如下所示(暫不考慮java8的紅黑樹方式):

當一個元素過來時,HashMap會先根據(jù)key的hashCode計算出需要存放在哪個位置,比如鍵a本來的hashCode的值為47,通過 mod 4(取余數(shù))計算得到可以存放在位置3上,然后發(fā)現(xiàn)位置3不為空,找到位置3的鏈表,然后對里面的元素依次進行equals比較,發(fā)現(xiàn)相同的則認為是更新,否則則認為是新增一個節(jié)點,會將新的節(jié)點放在這個鏈表的頭部。
hashCode不正確對HashMap的影響
從上面的簡單介紹可以發(fā)現(xiàn),HashMap會使用hashCode作為其判斷桶里面位置的依據(jù),如果hashCode不正確,可能會有以下問題:
- 只實現(xiàn)了
equals沒有定義hashCode,可能會導(dǎo)致put和get不正常,不是以想要的方式運行。 - 實現(xiàn)的
hashCode不均勻散列,導(dǎo)致數(shù)據(jù)都堆到一個位置,影響HashMap的性能。
下面詳細介紹這兩種情況。
只實現(xiàn)了equals沒有定義hashCode
只實現(xiàn)了equals,hashCode就會是Object默認的實現(xiàn),一般是每個不同的對象的hashCode都不一樣。下面是一個簡單的例子:
class Person{
private String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person person)) return false;
return Objects.equals(name, person.name);
}
}
這個類只有一個字段,而且只實現(xiàn)了equals,下面是把這個類當成key,來存放數(shù)據(jù),我們測試一下:
public static void main(String[] args) {
Map<Person, String> testMap = new HashMap<>();
testMap.put(new Person("Zhang San"), "test val");
System.out.println(testMap.get(new Person("Zhang San")));
}
我們用new Person("Zhang San")作為key,存了一個值test val,然后嘗試取出這個值,結(jié)果卻大出所料:有時輸出為null,有時輸出test val,也就是說,會在某些情況下無法取出這個值。發(fā)生了什么呢?
還是用剛才的圖來說明一下發(fā)生了什么:

我們存入時,假設(shè)鍵new Person("Zhang San")的hashCode為1,存入位置1,取數(shù)據(jù)時,new了一個新的對象,hashCode為2(默認每個對象的hashCode都不一樣),它落到了位置2,這時發(fā)現(xiàn)位置2沒有數(shù)據(jù),就返回null了。當取數(shù)據(jù)new的對象的散列值為類似5這類時,計算的存放位置為1,發(fā)現(xiàn)1里面有數(shù)據(jù),把鏈表里面的數(shù)據(jù)取出來做比較,第一個equals就相等了,就可以返回test val。
所以,在實現(xiàn)equals時,一定要實現(xiàn)hashCode。
實現(xiàn)的hashCode不均勻散列
先說正確的實現(xiàn)方式:可以直接使用Objects.hash方法,實現(xiàn)的結(jié)果就比較好了。比如上面的例子可以實現(xiàn)為:return Objects.hash(name);。
在IDEA中,還可以直接在類里面使用快捷鍵Alt+Insert,在彈出窗口選擇equals() 和 hashCode(),直接一鍵生成。
繼續(xù)介紹不均勻的散列,比如我們實現(xiàn)了一個散列函數(shù): return 10。還是上面的例子,我們會發(fā)現(xiàn)所有的數(shù)據(jù)都會映射到位置2,結(jié)果查找數(shù)據(jù)就成了鏈表查找,時間復(fù)雜度從O(1)降低到了O(n),效率會非常差。
總結(jié)
綜上所述,實現(xiàn)equals就必須要實現(xiàn)hashCode方法,否則會導(dǎo)致基于hash的一些集合無法正確工作。