爲什麼要重寫hashcode和equals方法?

1.通過Hash算法來了解HashMap對象的高效性

我們首先了解下數據結構中的一個知識點:在一個長度爲n(假設是10000)的 線性表(假設是ArrayList)裏,存放着無序的數字;如果我們要找一個指定的數字,就不得不通過從頭到尾一次便利來查找,這樣的平均查找次數是除以2(這裏是5000)。

對於Hash表(這裏的Hash表純粹是數據結構上的概念,和Java無關)。它的平均查找次數接近於1,代價相當小,關鍵在Hash表中,存放在其中的數據和它的存儲位置是用Hash函數關聯的。

我們假設一個Hash函數是x*x%5。當然實際情況裏不可能用這麼簡單的hash函數,我們這裏純粹爲了說明方便,而hash表是一個長度爲11 的線性表。如果我們要把6放入其中,那麼我們首先會對6用hash函數計算一下,結果是1,所以我們就把6放入到索引號是1這個位置。同樣如果我麼要放數字7,經過hash函數計算,7的結果是4,那麼它將被放入索引是4的這個位置。這個效果如圖所示:

在這裏插入圖片描述
這樣做的好處非常明顯。比如我們要從中找6這個元素,我們可以先通過hash函數計算6的索引位置,然後直接從1號索引裏找到它了。

不過我們會遇到“hash值衝突”這個問題。比如經過hash函數計算後,7和8會有相同的hash值,對此Java的hashmap對象採用的是“鏈地址法”的解決方法。效果如下圖所示。
在這裏插入圖片描述

具體的做法是,爲所有hash值是i的對象建立一個同義詞鏈表。假設我們在放入8的時候,發現4號位置已經被佔,那麼就會新建一個鏈表節點放入8。同樣,如果我們要找8,那麼發現4號索引裏不是8,那會沿着鏈表一次查找。

雖然我們還是無法徹底避免hash值衝突的問題,但是hash函數設計合理,仍能保證同義詞鏈表的長度被控制在一個合理的範圍裏。

2.重寫equals和hashcode方法的原因

當我們用hashMap存入自定義的類時,如果補充協議這個自定義類的equals和hashCode方法,得到的結果會和我們預期的不一樣。我們來看WithoutHashCode.java這個例子。

在其中的第2到第18行,我們定義了一個Key類;在其中的第3行定義了唯一的一個屬性id。當前我們先註釋掉第9行的equals方法和第16行的hashCode方法。

2   class Key {
3       private Integer id;
4       public Integer getId()
5   {return id; }
6       public Key(Integer id)
7   {this.id = id;  }
8   //故意先註釋掉equals和hashCode方法
9   //  public boolean equals(Object o) {
10  //      if (o == null || !(o instanceof Key))
11  //      { return false; }
12  //      else
13  //      { return this.getId().equals(((Key) o).getId());}
14  //  }
15     
16  //  public int hashCode()
17  //  { return id.hashCode(); }
18  }
19 
20  public class WithoutHashCode {
21      public static void main(String[] args) {
22          Key k1 = new Key(1);
23          Key k2 = new Key(1);
24          HashMap<Key,String> hm = new HashMap<Key,String>();
25          hm.put(k1, "Key with id is 1");    
26          System.out.println(hm.get(k2));    
27      }
28  }

在main函數裏的第22和23行,我們定義了兩個Key對象,它們的id都是1,就好比它們是兩把相同的都能打開同一扇門的鑰匙。

在第24行裏,我們通過泛型創建了一個HashMap對象。它的鍵部分可以存放Key類型的對象,值部分可以存儲String類型的對象。

在第25行裏,我們通過put方法把k1和一串字符放入到hashMap裏; 而在第26行,我們想用k2去從HashMap裏得到值;這就好比我們想用k1這把鑰匙來鎖門,用k2來開門。這是符合邏輯的,但從當前結果看,26行的返回結果不是我們想象中的那個字符串,而是null。

原因有兩個,第一是沒有重寫hashCode方法,第二是沒有重寫equals方法。

當我們往HashMap裏放k1時,首先會調用Key這個類的hashCode方法計算它的hash值,隨後把k1放入hash值所指引的內存位置。

關鍵是我們沒有在Key裏定義hashCode方法。這裏調用的仍是Object類的hashCode方法(所有的類都是Object的子類),而Object類的hashCode方法返回的hash值其實是k1對象的內存地址(假設是1000)。
在這裏插入圖片描述
如果我們隨後是調用hm.get(k1),那麼我們會再次調用hashCode方法(還是返回k1的地址1000),隨後根據得到的hash值,能很快地找到k1。

但我們這裏的代碼是hm.get(k2),當我們調用Object類的hashCode方法(因爲Key裏沒定義)計算k2的hash值時,其實得到的是k2的內存地址(假設是2000)。由於k1和k2是兩個不同的對象,所以它們的內存地址一定不會相同,也就是說它們的hash值一定不同,這就是我們無法用k2的hash值去拿k1的原因。

當我們把第16和17行的hashCode方法的註釋去掉後,會發現它是返回id屬性的hashCode值,這裏k1和k2的id都是1,所以它們的hash值是相等的。

我們再來更正一下存k1和取k2的動作。存k1時,是根據它id的hash值,假設這裏是100,把k1對象放入到對應的位置。而取k2時,是先計算它的hash值(由於k2的id也是1,這個值也是100),隨後到這個位置去找。

但結果會出乎我們意料:明明100號位置已經有k1,但第26行的輸出結果依然是null。其原因就是沒有重寫Key對象的equals方法。

HashMap是用鏈地址法來處理衝突,也就是說,在100號位置上,有可能存在着多個用鏈表形式存儲的對象。它們通過hashCode方法返回的hash值都是100。

在這裏插入圖片描述
當我們通過k2的hashCode到100號位置查找時,確實會得到k1。但k1有可能僅僅是和k2具有相同的hash值,但未必和k2相等(k1和k2兩把鑰匙未必能開同一扇門),這個時候,就需要調用Key對象的equals方法來判斷兩者是否相等了。

由於我們在Key對象裏沒有定義equals方法,系統就不得不調用Object類的equals方法。由於Object的固有方法是根據兩個對象的內存地址來判斷,所以k1和k2一定不會相等,這就是爲什麼依然在26行通過hm.get(k2)依然得到null的原因。

爲了解決這個問題,我們需要打開第9到14行equals方法的註釋。在這個方法裏,只要兩個對象都是Key類型,而且它們的id相等,它們就相等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章