為什麼重寫了equals()也要重寫hashCode()

筆者文筆功力尚淺,如有不妥,請慷慨指出,必定感激不盡

在Effective Java中第九條規定在覆蓋equals()方法時總要覆蓋hashCode()方法。這是為什麼呢?接下來我們就介紹一下這兩個方法。

Java中的equals()方法和hashCode()方法都是在Object類中的方法,而在Java中所有的類都是Obejct類的子類,所以Java中所有的方法都會有這兩個方法的默認實現。

equals方法

Object類中的equals()方法定義如下

1public boolean equals(Object obj) {
2 return (this == obj);
3}

我們發現在equals()方法中就關鍵的==,那麼==在Java中有什麼含義呢,我們都知道在Java中分為基本數據類型和引用數據類型。那麼==在這兩個類型中作用是不一樣的。

  • 基本數據類型:比較的是==兩邊值是否相等
  • 引用數據類型:比較的是==兩邊內存地址是否相等

基本數據類型包括:byte,short,char,int,long,float,double,boolean

而通過Java文檔中的equals()方法描述,所有要實現自己的equals()方法都要遵守下面幾個規則

  • 自反性:對於任何對象x,x.equals(x)應該返回true
  • 對稱性:對於任何兩個對象x和y,如果x.equals(y)返回true,那麼y.equals(x)也應該返回true
  • 傳遞性:對於多個對象x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,那麼y.equals(z)也應該返回true
  • 一致性:對於兩個非空對象x,y,在沒有修改此對象的前提下,多次調用返回的結果應該相同
  • 對於任何非空的對象x,x.equals(null)都應該返回false

hashCode方法

Object中的hashCode()方法是一個本地方法,返回一個int類型的哈希值。

1public native int hashCode();

在hashCode()方法中也有一些規約

  • 如果對象在使用equals方法中進行比較的參數沒有修改,那麼多次調用一個對象的hashCode()方法返回的哈希值應該是相同的。
  • 如果兩個對象通過equals方法比較是相等的,那麼要求這兩個對象的hashCode方法返回的值也應該是相等的。
  • 如果兩個對象通過equals方法比較是不同的,那麼也不要求這兩個對象的hashCode方法返回的值是相同的。但是我們應該知道對於不同對象產生不同的哈希值對於哈希表(HashMap等等)能夠提高性能。

equals方法和hashCode方法會在哪用到

這兩個方法經常出現在Java中的哪個類裡面呢?如果看過HashMap源碼的應該瞭解這兩個方法經常出現在HashMap中。網上介紹HashMap類的文章有很多了,這裡就簡單介紹一下HashMap。

當一個節點中的鏈表超過了8的時候就會變為紅黑樹,以解決鏈表長度過長以後查詢速度慢的缺點。

為什麼重寫了equals()也要重寫hashCode()

HashMap是由數組和鏈表組成的高效存儲數據的結構。那麼是如何確定一個數據存儲在數組中的哪個位置呢?就是通過hashCode方法進行計算出存儲在哪個位置,還記得我們上面講hashCode方法說了有可能兩個不同對象的hashCode方法返回的值相同,那麼此時就會產生衝突,產生衝突的話就會調用equals方法進行比對,如果不同,那麼就將其加入鏈表尾部,如果相同就替換原數據。

計算位置當然不是上面簡單的一個hashCode方法就計算出來,中間還有一些其他的步驟,這裡可以簡單的認為是hashCode確定了位置。

什麼時候去覆蓋這兩個方法呢?

如果你不將自定義的類定義為HashMap的key值的話,那麼我們重寫了equals方法而沒有重寫hashCode方法,編譯器不會報任何錯,在運行時也不會拋任何異常。

如果你想將自定義的類定義為HashMap的key值得話,那麼如果重寫了equals方法那麼就必須也重寫hashCode方法。

接下來我們可以看一下我們使用自定義的類作為HashMap的key,並且自定義的類不重寫equals和hashCode方法會發生什麼。

自定義的類

1@Builder
2@NoArgsConstructor
3@AllArgsConstructor
4class CustomizedKey{
5 private Integer id;
6 private String name;
7}

接下來我們看使用自定義的類作為key

 1 public static void main(String[] args) {
2
3 Map<customizedkey> data = getData();
4
5 CustomizedKey key = CustomizedKey.builder().id(1).name("key").build();
6
7 Integer integer = data.get(key);
8
9 System.out.printf(String.valueOf(integer));
10 }
11
12 private static Map<customizedkey> getData(){
13 Map<customizedkey> customizedKeyIntegerMap = new HashMap<>();
14 CustomizedKey key = CustomizedKey.builder().id(1).name("key").build();
15 customizedKeyIntegerMap.put(key,10);
16 return customizedKeyIntegerMap;
17 }
/<customizedkey>/<customizedkey>/<customizedkey>

我們可以看到程序最後打印的是一個null值。原因正如上面我們說的一樣。

  • hashCode:用來計算該對象放入數組中的哪個位置,因為是兩個都是new的對象,所以即使裡面的值一樣,但是對象所處的地址卻不同,所以使用默認的hashCode也就不同,當然在hashMap中就不會認為兩個是一個對象。

接下來我們就重寫一下這兩個方法。如果我們使用IDEA的話,那麼直接使用快捷鍵即可。

為什麼重寫了equals()也要重寫hashCode()

接下來我們看我們實現的兩個方法

 1@Builder
2@NoArgsConstructor
3@AllArgsConstructor
4class CustomizedKey{
5 private Integer id;
6 private String name;
7
8 @Override
9 public boolean equals(Object o) {
10 if (this == o) return true;
11 if (o == null || getClass() != o.getClass()) return false;
12 CustomizedKey that = (CustomizedKey) o;
13 return Objects.equals(id, that.id) &&
14 Objects.equals(name, that.name);
15 }
16
17 @Override
18 public int hashCode() {
19 return Objects.hash(id, name);
20 }
21}

然後我們再次運行上面的程序發現輸出打印已經變成了10。

我們也能夠使用Lombok提供的@EqualsAndHashCode註解簡化代碼


鏈接:https://juejin.im/post/5ddde269e51d4532c8596851
來源:掘金


分享到:


相關文章: