欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

 更新時(shí)間:2016年10月31日 15:33:33   作者:尼古拉斯_趙四  
本篇文章詳細(xì)介紹了Java中的equals和hashCode方法詳解,Object 類是所有類的父類,非常具有實(shí)用價(jià)值,需要的朋友可以參考下。

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

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

這里我們首先要明白一個(gè)問題:

equals()相等的兩個(gè)對象,hashcode()一定相等,equals()不相等的兩個(gè)對象,卻并不能證明他們的hashcode()不相等。換句話說,equals()方法不相等的兩個(gè)對象,hashCode()有可能相等。(我的理解是由于哈希碼在生成的時(shí)候產(chǎn)生沖突造成的)

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

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

同時(shí)hash算法對于查找元素提供了很高的效率

如果想查找一個(gè)集合中是否包含有某個(gè)對象,大概的程序代碼怎樣寫呢?

你通常是逐一取出每個(gè)元素與要查找的對象進(jìn)行比較,當(dāng)發(fā)現(xiàn)某個(gè)元素與要查找的對象進(jìn)行equals方法比較的結(jié)果相等時(shí),則停止繼續(xù)查找并返回肯定的信息,否則,返回否定的信息,如果一個(gè)集合中有很多個(gè)元素,比如有一萬個(gè)元素,并且沒有包含要查找的對象時(shí),則意味著你的程序需要從集合中取出一萬個(gè)元素進(jìn)行逐一比較才能得到結(jié)論。

有人發(fā)明了一種哈希算法來提高從集合中查找元素的效率,這種方式將集合分成若干個(gè)存儲區(qū)域,每個(gè)對象可以計(jì)算出一個(gè)哈希碼,可以將哈希碼分組(使用不同的hash函數(shù)來計(jì)算的),每組分別對應(yīng)某個(gè)存儲區(qū)域,根據(jù)一個(gè)對象的哈希嗎就可以確定該對象應(yīng)該存儲在哪個(gè)區(qū)域HashSet就是采用哈希算法存取對象的集合,它內(nèi)部采用對某個(gè)數(shù)字n進(jìn)行取余(這種的hash函數(shù)是最簡單的)的方式對哈希碼進(jìn)行分組和劃分對象的存儲區(qū)域;Object類中定義了一個(gè)hashCode()方法來返回每個(gè)Java對象的哈希碼,當(dāng)從HashSet集合中查找某個(gè)對象時(shí),Java系統(tǒng)首先調(diào)用對象的hashCode()方法獲得該對象的哈希碼表,然后根據(jù)哈希嗎找到相應(yīng)的存儲區(qū)域,最后取得該存儲區(qū)域內(nèi)的每個(gè)元素與該對象進(jìn)行equals方法比較;這樣就不用遍歷集合中的所有元素就可以得到結(jié)論,可見,HashSet集合具有很好的對象檢索性能,但是,HashSet集合存儲對象的效率相對要低些,因?yàn)橄騂ashSet集合中添加一個(gè)對象時(shí),要先計(jì)算出對象的哈希碼和根據(jù)這個(gè)哈希碼確定對象在集合中的存放位置為了保證一個(gè)類的實(shí)例對象能在HashSet正常存儲,要求這個(gè)類的兩個(gè)實(shí)例對象用equals()方法比較的結(jié)果相等時(shí),他們的哈希碼也必須相等;也就是說,如果obj1.equals(obj2)的結(jié)果為true,那么以下表達(dá)式的結(jié)果也要為true:
obj1.hashCode() == obj2.hashCode()

換句話說:當(dāng)我們重寫一個(gè)對象的equals方法,就必須重寫他的hashCode方法,不過不重寫他的hashCode方法的話,Object對象中的hashCode方法始終返回的是一個(gè)對象的hash地址,而這個(gè)地址是永遠(yuǎn)不相等的。所以這時(shí)候即使是重寫了equals方法,也不會有特定的效果的,因?yàn)閔ashCode方法如果都不想等的話,就不會調(diào)用equals方法進(jìn)行比較了,所以沒有意義了。

如果一個(gè)類的hashCode()方法沒有遵循上述要求,那么,當(dāng)這個(gè)類的兩個(gè)實(shí)例對象用equals()方法比較的結(jié)果相等時(shí),他們本來應(yīng)該無法被同時(shí)存儲進(jìn)set集合中,但是,如果將他們存儲進(jìn)HashSet集合中時(shí),由于他們的hashCode()方法的返回值不同(Object中的hashCode方法返回值是永遠(yuǎn)不同的),第二個(gè)對象首先按照哈希碼計(jì)算可能被放進(jìn)與第一個(gè)對象不同的區(qū)域中,這樣,它就不可能與第一個(gè)對象進(jìn)行equals方法比較了,也就可能被存儲進(jìn)HashSet集合中了,Object類中的hashCode()方法不能滿足對象被存入到HashSet中的要求,因?yàn)樗姆祷刂凳峭ㄟ^對象的內(nèi)存地址推算出來的,同一個(gè)對象在程序運(yùn)行期間的任何時(shí)候返回的哈希值都是始終不變的,所以,只要是兩個(gè)不同的實(shí)例對象,即使他們的equals方法比較結(jié)果相等,他們默認(rèn)的hashCode方法的返回值是不同的。

下面來看一下一個(gè)具體的例子:

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方法中,如果兩個(gè)RectObject對象的x,y值相等的話他們的hashCode值是相等的,同時(shí)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中存入到了四個(gè)對象,打印set集合的大小,結(jié)果是多少呢?

運(yùn)行結(jié)果:size:2

為什么會是2呢?這個(gè)很簡單了吧,因?yàn)槲覀冎貙懥薘ectObject類的hashCode方法,只要RectObject對象的x,y屬性值相等那么他的hashCode值也是相等的,所以先比較hashCode的值,r1和r2對象的x,y屬性值不等,所以他們的hashCode不相同的,所以r2對象可以放進(jìn)去,但是r3對象的x,y屬性值和r1對象的屬性值相同的,所以hashCode是相等的,這時(shí)候在比較r1和r3的equals方法,因?yàn)樗磧傻膞,y值是相等的,所以r1,r3對象是相等的,所以r3不能放進(jìn)去了,同樣最后再添加一個(gè)r1也是沒有沒有添加進(jìn)去的,所以set集合中只有一個(gè)r1和r2這兩個(gè)對象

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

運(yùn)行結(jié)果:size:3

這個(gè)結(jié)果也是很簡單的,首先判斷r1對象和r2對象的hashCode,因?yàn)镺bject中的hashCode方法返回的是對象本地內(nèi)存地址的換算結(jié)果,不同的實(shí)例對象的hashCode是不相同的,同樣因?yàn)閞3和r1的hashCode也是不相等的,但是r1==r1的,所以最后set集合中只有r1,r2,r3這三個(gè)對象,所以大小是3

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

運(yùn)行結(jié)果:size:3

這個(gè)結(jié)果就有點(diǎn)意外了,我們來分析一下:

首先r1和r2的對象比較hashCode,不相等,所以r2放進(jìn)set中,再來看一下r3,比較r1和r3的hashCode方法,是相等的,然后比較他們兩的equals方法,因?yàn)閑quals方法始終返回false,所以r1和r3也是不相等的,r3和r2就不用說了,他們兩的hashCode是不相等的,所以r3放進(jìn)set中,再看r4,比較r1和r4發(fā)現(xiàn)hashCode是相等的,在比較equals方法,因?yàn)閑quals返回false,所以r1和r4不相等,同一r2和r4也是不相等的,r3和r4也是不相等的,所以r4可以放到set集合中,那么結(jié)果應(yīng)該是size:4,那為什么會是3呢?

這時(shí)候我們就需要查看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; 
  } 

這里我們可以看到其實(shí)HashSet是基于HashMap實(shí)現(xiàn)的,我們在點(diǎn)擊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是否相等,不相等的話,直接跳過,相等的話,然后再來比較這兩個(gè)對象是否相等或者這兩個(gè)對象的equals方法,因?yàn)槭沁M(jìn)行的或操作,所以只要有一個(gè)成立即可,那這里我們就可以解釋了,其實(shí)上面的那個(gè)集合的大小是3,因?yàn)樽詈蟮囊粋€(gè)r1沒有放進(jìn)去,以為r1==r1返回true的,所以沒有放進(jìn)去了。所以集合的大小是3,如果我們將hashCode方法設(shè)置成始終返回false的話,這個(gè)集合就是4了。

最后我們在來看一下hashCode造成的內(nèi)存泄露的問題:看一下代碼:

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()); 
  } 
} 

運(yùn)行結(jié)果:

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

擦,發(fā)現(xiàn)一個(gè)問題了,而且是個(gè)大問題呀,我們調(diào)用了remove刪除r3對象,以為刪除了r3,但事實(shí)上并沒有刪除,這就叫做內(nèi)存泄露,就是不用的對象但是他還在內(nèi)存中。所以我們多次這樣操作之后,內(nèi)存就爆了。看一下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; 
  } 

我們看到,在調(diào)用remove方法的時(shí)候,會先使用對象的hashCode值去找到這個(gè)對象,然后進(jìn)行刪除,這種問題就是因?yàn)槲覀冊谛薷牧藃3對象的y屬性的值,又因?yàn)镽ectObject對象的hashCode方法中有y值參與運(yùn)算,所以r3對象的hashCode就發(fā)生改變了,所以remove方法中并沒有找到r3了,所以刪除失敗。即r3的hashCode變了,但是他存儲的位置沒有更新,仍然在原來的位置上,所以當(dāng)我們用他的新的hashCode去找肯定是找不到了。
其實(shí)上面的方法實(shí)現(xiàn)很簡單的:如下圖:

很簡單的一個(gè)線性的hash表,使用的hash函數(shù)是mod,源碼如下:

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

這個(gè)其實(shí)就是mod運(yùn)算,只是這種運(yùn)算比%運(yùn)算要高效。

1,2,3,4,5表示是mod的結(jié)果,每個(gè)元素對應(yīng)的是一個(gè)鏈表結(jié)構(gòu),所以說想刪除一個(gè)Entry<K,V>的話,首先得到hashCode,從而獲取到鏈表的頭結(jié)點(diǎn),然后再遍歷這個(gè)鏈表,如果hashCode和equals相等就刪除這個(gè)元素。
上面的這個(gè)內(nèi)存泄露告訴我一個(gè)信息:如果我們將對象的屬性值參與了hashCode的運(yùn)算中,在進(jìn)行刪除的時(shí)候,就不能對其屬性值進(jìn)行修改,否則會出現(xiàn)嚴(yán)重的問題。

其實(shí)我們也可以看一下8種基本數(shù)據(jù)類型對應(yīng)的對象類型和String類型的hashCode方法和equals方法。

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

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。

相關(guān)文章

  • 如何有效管理JVM中的垃圾?

    如何有效管理JVM中的垃圾?

    今天給大家?guī)淼氖顷P(guān)于Java虛擬機(jī)的相關(guān)知識,文章圍繞著如何有效管理JVM中的垃圾展開,文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下
    2021-06-06
  • SparkSQL快速入門教程

    SparkSQL快速入門教程

    Spark?SQL是Spark用于結(jié)構(gòu)化數(shù)據(jù)(structured?data)處理的Spark模塊,今天通過本文給大家介紹SparkSQL快速入門教程,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧
    2022-02-02
  • SpringBoot添加富文本編輯器操作步驟

    SpringBoot添加富文本編輯器操作步驟

    富文本編輯器是一種能夠編輯和展示富文本內(nèi)容的工具或程序,與純文本編輯器不同,富文本編輯器可以處理文本的格式、樣式、布局等方面,使文本更加豐富多樣,本文給大家介紹了SpringBoot添加富文本編輯器操作步驟,需要的朋友可以參考下
    2024-01-01
  • 如何將Java枚舉名稱作為注解的屬性值實(shí)現(xiàn)詳解

    如何將Java枚舉名稱作為注解的屬性值實(shí)現(xiàn)詳解

    這篇文章主要為大家介紹了如何將Java枚舉名稱作為注解的屬性值實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-05-05
  • Java8函數(shù)式接口java.util.function速查大全

    Java8函數(shù)式接口java.util.function速查大全

    因?yàn)镴ava8引入了函數(shù)式接口,在java.util.function包含了幾大類函數(shù)式接口聲明,這篇文章主要給大家介紹了關(guān)于Java8函數(shù)式接口java.util.function速查的相關(guān)資料,需要的朋友可以參考下
    2021-08-08
  • Java文件管理操作的知識點(diǎn)整理

    Java文件管理操作的知識點(diǎn)整理

    這篇文章主要為大家詳細(xì)介紹了Java中文件管理操作的一些知識點(diǎn)和實(shí)現(xiàn)方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下
    2022-09-09
  • IDEA新建的Moudle失效顯示為灰色的完美解決方案

    IDEA新建的Moudle失效顯示為灰色的完美解決方案

    這篇文章主要介紹了IDEA新建的Moudle失效顯示為灰色,本文通過圖文并茂的形式給大家分享完美解決方案,需要的朋友可以參考下
    2023-09-09
  • java容器詳細(xì)解析

    java容器詳細(xì)解析

    本文主要介紹了java容器的詳細(xì)解析。具有很好的參考價(jià)值。下面跟著小編一起來看下吧
    2017-03-03
  • cascade級聯(lián)關(guān)系操作案例詳解

    cascade級聯(lián)關(guān)系操作案例詳解

    這篇文章主要介紹了cascade級聯(lián)關(guān)系,主要包括級聯(lián)保存,級聯(lián)修改,級聯(lián)刪除案例,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下
    2022-07-07
  • 詳解Java線程池隊(duì)列中的延遲隊(duì)列DelayQueue

    詳解Java線程池隊(duì)列中的延遲隊(duì)列DelayQueue

    這篇文章主要為大家詳細(xì)介紹了Java線程池隊(duì)列中的延遲隊(duì)列DelayQueue的相關(guān)資料,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2022-12-12

最新評論