java集合——Java中的equals和hashCode方法詳解_java

來源:互聯網
上載者:User

Java中的equals方法和hashCode方法是Object中的,所以每個對象都是有這兩個方法的,有時候我們需要實現特定需求,可能要重寫這兩個方法,今天就來介紹一些這兩個方法的作用。

equals()和hashCode()方法是用來在同一類中做比較用的,尤其是在容器裡如set存放同一類對象時用來判斷放入的對象是否重複。

這裡我們首先要明白一個問題:

equals()相等的兩個對象,hashcode()一定相等,equals()不相等的兩個對象,卻並不能證明他們的hashcode()不相等。換句話說,equals()方法不相等的兩個對象,hashCode()有可能相等。(我的理解是由於雜湊碼在產生的時候產生衝突造成的)

在這裡hashCode就好比字典裡每個字的索引,equals()好比比較的是字典裡同一個字下的不同詞語。就好像在字典裡查“自”這個字下的兩個詞語“自己”、“自發”,如果用equals()判斷查詢的詞語相等那麼就是同一個詞語,比如equals()比較的兩個詞語都是“自己”,那麼此時hashCode()方法得到的值也肯定相等;如果用equals()方法比較的是“自己”和“自發”這兩個詞語,那麼得到結果是不想等,但是這兩個詞都屬於“自”這個字下的詞語所以在查索引時相同,即:hashCode()相同。如果用equals()比較的是“自己”和“他們”這兩個詞語的話那麼得到的結果也是不同的,此時hashCode() 得到也是不同的。

反過來:hashcode()不等,一定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等。在object類中,hashcode()方法是本地方法,返回的是對象的地址值,而object類中的equals()方法比較的也是兩個對象的地址值,如果equals()相等,說明兩個對象地址值也相等,當然hashcode() 也就相等了;

同時hash演算法對於尋找元素提供了很高的效率

如果想尋找一個集合中是否包含有某個對象,大概的程式碼怎樣寫呢?

你通常是逐一取出每個元素與要尋找的對象進行比較,當發現某個元素與要尋找的對象進行equals方法比較的結果相等時,則停止繼續尋找並返回肯定的資訊,否則,返回否定的資訊,如果一個集合中有很多個元素,比如有一萬個元素,並且沒有包含要尋找的對象時,則意味著你的程式需要從集合中取出一萬個元素進行逐一比較才能得到結論。

有人發明了一種雜湊演算法來提高從集合中尋找元素的效率,這種方式將集合分成若干個儲存地區,每個對象可以計算出一個雜湊碼,可以將雜湊碼分組(使用不同的hash函數來計算的),每組分別對應某個儲存地區,根據一個對象的雜湊嗎就可以確定該對象應該儲存在哪個地區HashSet就是採用雜湊演算法存取對象的集合,它內部採用對某個數字n進行取餘(這種的hash函數是最簡單的)的方式對雜湊碼進行分組和劃分對象的儲存地區;Object類中定義了一個hashCode()方法來返回每個Java對象的雜湊碼,當從HashSet集合中尋找某個對象時,Java系統首先調用對象的hashCode()方法獲得該對象的雜湊碼錶,然後根據雜湊嗎找到相應的儲存地區,最後取得該儲存地區內的每個元素與該對象進行equals方法比較;這樣就不用遍曆集合中的所有元素就可以得到結論,可見,HashSet集合具有很好的對象檢索效能,但是,HashSet集合儲存物件的效率相對要低些,因為向HashSet集合中添加一個對象時,要先計算出對象的雜湊碼和根據這個雜湊碼確定對象在集合中的存放位置為了保證一個類的執行個體對象能在HashSet正常儲存,要求這個類的兩個執行個體對象用equals()方法比較的結果相等時,他們的雜湊碼也必須相等;也就是說,如果obj1.equals(obj2)的結果為true,那麼以下運算式的結果也要為true:
obj1.hashCode() == obj2.hashCode()

換句話說:當我們重寫一個對象的equals方法,就必須重寫他的hashCode方法,不過不重寫他的hashCode方法的話,Object對象中的hashCode方法始終返回的是一個對象的hash地址,而這個地址是永遠不相等的。所以這時候即使是重寫了equals方法,也不會有特定的效果的,因為hashCode方法如果都不想等的話,就不會調用equals方法進行比較了,所以沒有意義了。

如果一個類的hashCode()方法沒有遵循上述要求,那麼,當這個類的兩個執行個體對象用equals()方法比較的結果相等時,他們本來應該無法被同時儲存進set集合中,但是,如果將他們儲存進HashSet集合中時,由於他們的hashCode()方法的傳回值不同(Object中的hashCode方法傳回值是永遠不同的),第二個對象首先按照雜湊碼計算可能被放進與第一個對象不同的地區中,這樣,它就不可能與第一個對象進行equals方法比較了,也就可能被儲存進HashSet集合中了,Object類中的hashCode()方法不能滿足對象被存入到HashSet中的要求,因為它的傳回值是通過對象的記憶體位址推算出來的,同一個對象在程式運行期間的任何時候返回的雜湊值都是始終不變的,所以,只要是兩個不同的執行個體對象,即使他們的equals方法比較結果相等,他們預設的hashCode方法的傳回值是不同的。

下面來看一下一個具體的例子:

RectObject對象:package com.weijia.demo;  public class RectObject {   public int x;   public int y;   public RectObject(int x,int y){     this.x = x;     this.y = y;   }   @Override   public int hashCode(){     final int prime = 31;     int result = 1;     result = prime * result + x;     result = prime * result + y;     return result;   }   @Override   public boolean equals(Object obj){     if(this == obj)       return true;     if(obj == null)       return false;     if(getClass() != obj.getClass())       return false;     final RectObject other = (RectObject)obj;     if(x != other.x){       return false;     }     if(y != other.y){       return false;     }     return true;   } } 

我們重寫了父類Object中的hashCode和equals方法,看到hashCode和equals方法中,如果兩個RectObject對象的x,y值相等的話他們的hashCode值是相等的,同時equals返回的是true;

下面是測試代碼:

package com.weijia.demo; import java.util.HashSet; public class Demo {   public static void main(String[] args){     HashSet<RectObject> set = new HashSet<RectObject>();     RectObject r1 = new RectObject(3,3);     RectObject r2 = new RectObject(5,5);     RectObject r3 = new RectObject(3,3);     set.add(r1);     set.add(r2);     set.add(r3);     set.add(r1);     System.out.println("size:"+set.size());   } } 

我們向HashSet中存入到了四個對象,列印set集合的大小,結果是多少呢?

運行結果:size:2

為什麼會是2呢?這個很簡單了吧,因為我們重寫了RectObject類的hashCode方法,只要RectObject對象的x,y屬性值相等那麼他的hashCode值也是相等的,所以先比較hashCode的值,r1和r2對象的x,y屬性值不等,所以他們的hashCode不相同的,所以r2對象可以放進去,但是r3對象的x,y屬性值和r1對象的屬性值相同的,所以hashCode是相等的,這時候在比較r1和r3的equals方法,因為他麼兩的x,y值是相等的,所以r1,r3對象是相等的,所以r3不能放進去了,同樣最後再添加一個r1也是沒有沒有添加進去的,所以set集合中只有一個r1和r2這兩個對象

下面我們把RectObject對象中的hashCode方法注釋,即不重寫Object對象中的hashCode方法,在運行一下代碼:

運行結果:size:3

這個結果也是很簡單的,首先判斷r1對象和r2對象的hashCode,因為Object中的hashCode方法返回的是對象本地記憶體位址的換算結果,不同的執行個體對象的hashCode是不相同的,同樣因為r3和r1的hashCode也是不相等的,但是r1==r1的,所以最後set集合中只有r1,r2,r3這三個對象,所以大小是3

下面我們把RectObject對象中的equals方法中的內容注釋,直接返回false,不注釋hashCode方法,運行一下代碼:

運行結果:size:3

這個結果就有點意外了,我們來分析一下:

首先r1和r2的對象比較hashCode,不相等,所以r2放進set中,再來看一下r3,比較r1和r3的hashCode方法,是相等的,然後比較他們兩的equals方法,因為equals方法始終返回false,所以r1和r3也是不相等的,r3和r2就不用說了,他們兩的hashCode是不相等的,所以r3放進set中,再看r4,比較r1和r4發現hashCode是相等的,在比較equals方法,因為equals返回false,所以r1和r4不相等,同一r2和r4也是不相等的,r3和r4也是不相等的,所以r4可以放到set集合中,那麼結果應該是size:4,那為什麼會是3呢?

這時候我們就需要查看HashSet的源碼了,下面是HashSet中的add方法的源碼:

/**    * Adds the specified element to this set if it is not already present.    * More formally, adds the specified element <tt>e</tt> to this set if    * this set contains no element <tt>e2</tt> such that    * <tt>(e==null ? e2==null : e.equals(e2))</tt>.    * If this set already contains the element, the call leaves the set    * unchanged and returns <tt>false</tt>.    *    * @param e element to be added to this set    * @return <tt>true</tt> if this set did not already contain the specified    * element    */   public boolean add(E e) {     return map.put(e, PRESENT)==null;   } 

這裡我們可以看到其實HashSet是基於HashMap實現的,我們在點擊HashMap的put方法,源碼如下:

/**    * Associates the specified value with the specified key in this map.    * If the map previously contained a mapping for the key, the old    * value is replaced.    *    * @param key key with which the specified value is to be associated    * @param value value to be associated with the specified key    * @return the previous value associated with <tt>key</tt>, or    *     <tt>null</tt> if there was no mapping for <tt>key</tt>.    *     (A <tt>null</tt> return can also indicate that the map    *     previously associated <tt>null</tt> with <tt>key</tt>.)    */   public V put(K key, V value) {     if (key == null)       return putForNullKey(value);     int hash = hash(key);     int i = indexFor(hash, table.length);     for (Entry<K,V> e = table[i]; e != null; e = e.next) {       Object k;       if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {         V oldValue = e.value;         e.value = value;         e.recordAccess(this);         return oldValue;       }     }      modCount++;     addEntry(hash, key, value, i);     return null;   } 

我們主要來看一下if的判斷條件,

首先是判斷hashCode是否相等,不相等的話,直接跳過,相等的話,然後再來比較這兩個對象是否相等或者這兩個對象的equals方法,因為是進行的或操作,所以只要有一個成立即可,那這裡我們就可以解釋了,其實上面的那個集合的大小是3,因為最後的一個r1沒有放進去,以為r1==r1返回true的,所以沒有放進去了。所以集合的大小是3,如果我們將hashCode方法設定成始終返回false的話,這個集合就是4了。

最後我們在來看一下hashCode造成的記憶體泄露的問題:看一下代碼:

package com.weijia.demo; import java.util.HashSet; public class Demo {   public static void main(String[] args){     HashSet<RectObject> set = new HashSet<RectObject>();     RectObject r1 = new RectObject(3,3);     RectObject r2 = new RectObject(5,5);     RectObject r3 = new RectObject(3,3);     set.add(r1);     set.add(r2);     set.add(r3);     r3.y = 7;     System.out.println("刪除前的大小size:"+set.size());     set.remove(r3);     System.out.println("刪除後的大小size:"+set.size());   } } 

運行結果:

刪除前的大小size:3
刪除後的大小size:3

擦,發現一個問題了,而且是個大問題呀,我們調用了remove刪除r3對象,以為刪除了r3,但事實上並沒有刪除,這就叫做記憶體泄露,就是不用的對象但是他還在記憶體中。所以我們多次這樣操作之後,記憶體就爆了。看一下remove的源碼:

/**    * Removes the specified element from this set if it is present.    * More formally, removes an element <tt>e</tt> such that    * <tt>(o==null ? e==null : o.equals(e))</tt>,    * if this set contains such an element. Returns <tt>true</tt> if    * this set contained the element (or equivalently, if this set    * changed as a result of the call). (This set will not contain the    * element once the call returns.)    *    * @param o object to be removed from this set, if present    * @return <tt>true</tt> if the set contained the specified element    */   public boolean remove(Object o) {     return map.remove(o)==PRESENT;   } 

然後再看一下remove方法的源碼:

/**    * Removes the mapping for the specified key from this map if present.    *    * @param key key whose mapping is to be removed from the map    * @return the previous value associated with <tt>key</tt>, or    *     <tt>null</tt> if there was no mapping for <tt>key</tt>.    *     (A <tt>null</tt> return can also indicate that the map    *     previously associated <tt>null</tt> with <tt>key</tt>.)    */   public V remove(Object key) {     Entry<K,V> e = removeEntryForKey(key);     return (e == null ? null : e.value);   } 

在看一下removeEntryForKey方法源碼:

/**    * Removes and returns the entry associated with the specified key    * in the HashMap. Returns null if the HashMap contains no mapping    * for this key.    */   final Entry<K,V> removeEntryForKey(Object key) {     int hash = (key == null) ? 0 : hash(key);     int i = indexFor(hash, table.length);     Entry<K,V> prev = table[i];     Entry<K,V> e = prev;      while (e != null) {       Entry<K,V> next = e.next;       Object k;       if (e.hash == hash &&         ((k = e.key) == key || (key != null && key.equals(k)))) {         modCount++;         size--;         if (prev == e)           table[i] = next;         else           prev.next = next;         e.recordRemoval(this);         return e;       }       prev = e;       e = next;     }      return e;   } 

我們看到,在調用remove方法的時候,會先使用對象的hashCode值去找到這個對象,然後進行刪除,這種問題就是因為我們在修改了r3對象的y屬性的值,又因為RectObject對象的hashCode方法中有y值參與運算,所以r3對象的hashCode就發生改變了,所以remove方法中並沒有找到r3了,所以刪除失敗。即r3的hashCode變了,但是他儲存的位置沒有更新,仍然在原來的位置上,所以當我們用他的新的hashCode去找肯定是找不到了。
其實上面的方法實現很簡單的:如下圖:

很簡單的一個線性hash表,使用的hash函數是mod,源碼如下:

/**   * Returns index for hash code h.   */   static int indexFor(int h, int length) {     return h & (length-1);   } 

這個其實就是mod運算,只是這種運算比%運算要高效。

1,2,3,4,5表示是mod的結果,每個元素對應的是一個鏈表結構,所以說想刪除一個Entry<K,V>的話,首先得到hashCode,從而擷取到鏈表的頭結點,然後再遍曆這個鏈表,如果hashCode和equals相等就刪除這個元素。
上面的這個記憶體泄露告訴我一個資訊:如果我們將對象的屬性值參與了hashCode的運算中,在進行刪除的時候,就不能對其屬性值進行修改,否則會出現嚴重的問題。

其實我們也可以看一下8種基礎資料型別 (Elementary Data Type)對應的物件類型和String類型的hashCode方法和equals方法。

其中8中基本類型的hashCode很簡單就是直接返回他們的數值大小,String對象是通過一個複雜的計算方式,但是這種計算方式能夠保證,如果這個字串的值相等的話,他們的hashCode就是相等的。8種基本類型的equals方法就是直接比較數值,String類型的equals方法是比較字串的值的。

以上就是本文的全部內容,希望對大家的學習有所協助,也希望大家多多支援雲棲社區。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.