hashCode和equals的區別


關注公眾號,大家可以在公眾號后台回復“博客園”,免費獲得作者 Java 知識體系/面試必看資料。


有面試官會問:你重寫過 hashcode 和 equals 么,為什么重寫equals時必須重寫hashCode方法?equals和hashCode都是Object對象中的非final方法,它們設計的目的就是被用來覆蓋(override)的,所以在程序設計中還是經常需要處理這兩個方法。下面我們一起來看一下,它們到底有什么區別,總結一波!

 

 01、hashCode介紹

hashCode() 的作用是獲取哈希碼,也稱為散列碼;它實際上是返回一個int整數。這個哈希碼的作用是確定該對象在哈希表中的索引位置。hashCode() 定義在JDK的Object.java中,這就意味着Java中的任何類都包含有hashCode() 函數。

舉個例子

public class DemoTest {​ public static void main(String[] args) { Object obj = new Object(); System.out.println(obj.hashCode()); }}

 

通過調用hashCode()方法獲取對象的hash值。

 

02、equals介紹

equals它的作用也是判斷兩個對象是否相等,如果對象重寫了equals()方法,比較兩個對象的內容是否相等;如果沒有重寫,比較兩個對象的地址是否相同,價於“==”。同樣的,equals()定義在JDK的Object.java中,這就意味着Java中的任何類都包含有equals()函數。

 舉個例子

public class DemoTest {​ public static void main(String[] args) { Object obj = new Object(); System.out.println(obj.equals(obj)); }}

 

 

03、hashCode() 和 equals() 有什么關系?

接下面,我們討論另外一個話題。網上很多文章將 hashCode() 和 equals 關聯起來,有的講的不透徹,有誤導讀者的嫌疑。在這里,我們梳理了一下 “hashCode() 和 equals()的關系”。我們以“類的用途”來將“hashCode() 和 equals()的關系”分2種情況來說明。

3.1、不會創建“類對應的散列表”

這里所說的“不會創建類對應的散列表”是說:我們不會在HashSet, HashTable, HashMap等等這些本質是散列表的數據結構中,用到該類。例如,不會創建該類的HashSet集合。

 

在這種情況下,該類的“hashCode() 和 equals() ”沒有半毛錢關系的!

equals() 用來比較該類的兩個對象是否相等,而hashCode() 則根本沒有任何作用,所以,不用理會hashCode()。

舉個例子

public class DemoNormalTest {​  public static void main(String[] args) {    // 新建2個相同內容的Person對象,    // 再用equals比較它們是否相等    Person p1 = new Person("eee", 100);    Person p2 = new Person("eee", 100);    Person p3 = new Person("aaa", 200);​    System.out.printf("p1.equals(p2) : %s; p1(%d) p2(%d)\n", p1.equals(p2), p1.hashCode(), p2.hashCode());    System.out.printf("p1.equals(p3) : %s; p1(%d) p3(%d)\n", p1.equals(p3), p1.hashCode(), p3.hashCode());  }​  private static class Person {​    private String name;​    private int age;​    public Person(String name, int age) {      super();      this.name = name;      this.age = age;    }​    /**     * 重寫equals方法     */    @Override    public boolean equals(Object obj) {      if (obj == null) {        return false;      }​      // 如果是同一個對象返回true,反之返回false      if (this == obj) {        return true;      }​      // 判斷是否類型相同      if (this.getClass() != obj.getClass()) {        return false;      }      Person person = (Person) obj;      return name.equals(person.name) && age == person.age;    }  }}

 

運行結果:

p1.equals(p2) : true; p1(2018699554) p2(1311053135)p1.equals(p3) : false; p1(2018699554) p3(1735600054)

從結果也可以看出:p1和p2相等的情況下,hashCode()也不一定相等。

3.2、會創建“類對應的散列表”

這里所說的“會創建類對應的散列表”是說:我們會在HashSet, HashTable, HashMap等等這些本質是散列表的數據結構中,用到該類。例如,創建該類的HashSet集合。

 

在這種情況下,該類的“hashCode() 和 equals() ”是有關系的:

  • 如果兩個對象相等,那么它們的hashCode()值一定相同。這里的相等是指,通過equals()比較兩個對象時返回true。

  • 如果兩個對象hashCode()相等,它們並不一定相等。因為在散列表中,hashCode()相等,即兩個鍵值對的哈希值相等。然而哈希值相等,並不一定能得出鍵值對相等,此時就出現所謂的哈希沖突場景。

舉個例子

public class DemoConflictTest {​  public static void main(String[] args) {    // 新建Person對象,

Person p1 = new Person("eee", 100); Person p2 = new Person("eee", 100); Person p3 = new Person("aaa", 200);​ // 新建HashSet對象 HashSet<Person> set = new HashSet<>();        set.add(p1);        set.add(p2);        set.add(p3);​        // 比較p1 和 p2, 並打印它們的hashCode()        System.out.printf("p1.equals(p2) : %s; p1(%d) p2(%d)\n", p1.equals(p2), p1.hashCode(), p2.hashCode());        // 打印set        System.out.printf("set:%s\n", set);  }  private static class Person {​    private String name;​    private int age;​    public Person(String name, int age) {      super();      this.name = name;      this.age = age;    }​    /**     * 重寫toString方法     */    @Override    public String toString() {      return "("+name + ", " +age+")";    }​    /**     * 重寫equals方法     */    @Override    public boolean equals(Object obj) {      if (obj == null) {        return false;      }​      // 如果是同一個對象返回true,反之返回false      if (this == obj) {        return true;      }​      // 判斷是否類型相同      if (this.getClass() != obj.getClass()) {        return false;      }      Person person = (Person) obj;      return name.equals(person.name) && age == person.age;    }  }}

 

運行結果:

p1.equals(p2) : true; p1(2018699554) p2(1311053135)set:[(eee, 100), (aaa, 200), (eee, 100)]

 

結果分析:

我們重寫了Person的equals()。但是,很奇怪的發現:HashSet中仍然有重復元素:p1 和 p2。為什么會出現這種情況呢?

這是因為雖然p1 和 p2的內容相等,但是它們的hashCode()不等;所以,HashSet在添加p1和p2的時候,認為它們不相等。

 

舉個例子,我們同時覆蓋equals() 和 hashCode()方法。

public class DemoConflictTest {​  public static void main(String[] args) {    // 新建Person對象,    Person p1 = new Person("eee", 100);    Person p2 = new Person("eee", 100);    Person p3 = new Person("aaa", 200);    Person p4 = new Person("EEE", 100);​    // 新建HashSet對象    HashSet<Person> set = new HashSet<>();    set.add(p1);    set.add(p2);    set.add(p3);    set.add(p4);​    // 比較p1 和 p2, 並打印它們的hashCode()    System.out.printf("p1.equals(p2) : %s; p1(%d) p2(%d)\n", p1.equals(p2), p1.hashCode(), p2.hashCode());    // 比較p1 和 p4, 並打印它們的hashCode()    System.out.printf("p1.equals(p4) : %s; p1(%d) p4(%d)\n", p1.equals(p4), p1.hashCode(), p4.hashCode());    // 打印set    System.out.printf("set:%s\n", set);  }​  private static class Person {​    private String name;​    private int age;​    public Person(String name, int age) {      super();      this.name = name;      this.age = age;    }​    /**     * 重寫toString方法     */    @Override    public String toString() {      return "(" + name + ", " + age + ")";    }​    /**     * 重寫equals方法     */    @Override    public boolean equals(Object obj) {      if (obj == null) {        return false;      }​      // 如果是同一個對象返回true,反之返回false      if (this == obj) {        return true;      }​      // 判斷是否類型相同      if (this.getClass() != obj.getClass()) {        return false;      }      Person person = (Person) obj;      return name.equals(person.name) && age == person.age;    }​    /**     * 重寫hashCode方法     */    @Override    public int hashCode() {      int nameHash = name.toUpperCase().hashCode();      return nameHash ^ age;    }​  }}
運行結果:
p1.equals(p2) : true; p1(68545) p2(68545)p1.equals(p4) : false; p1(68545) p4(68545)set:[(eee, 100), (EEE, 100), (aaa, 200)]

結果分析:

這下,equals()生效了,HashSet中沒有重復元素。 比較p1和p2,我們發現:它們的hashCode()相等,通過equals()比較它們也返回true。所以,p1和p2被視為相等。 比較p1和p4,我們發現:雖然它們的hashCode()相等;但是,通過equals()比較它們返回false。所以,p1和p4被視為不相等。

 

為什么HashSet會用到hashCode()呢?

查看HashSet的源碼部分

/** * HashSet部分 */public boolean add(E e) {        return map.put(e, PRESENT)==null;}​​/** * map.put方法部分 */public V put(K key, V value) {        return putVal(hash(key), key, value, false, true);}​/** * putVal方法部分 */final V putVal(int hash, K key, V value, boolean onlyIfAbsent,                   boolean evict) {    Node<K,V>[] tab; Node<K,V> p; int n, i;    if ((tab = table) == null || (n = tab.length) == 0)        n = (tab = resize()).length;    if ((p = tab[i = (n - 1) & hash]) == null)        tab[i] = newNode(hash, key, value, null);    else {        Node<K,V> e; K k;        if (p.hash == hash &&            ((k = p.key) == key || (key != null && key.equals(k))))            e = p;        else if (p instanceof TreeNode)            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);        else {            for (int binCount = 0; ; ++binCount) {                if ((e = p.next) == null) {                    p.next = newNode(hash, key, value, null);                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st                        treeifyBin(tab, hash);                    break;                }                if (e.hash == hash &&                    ((k = e.key) == key || (key != null && key.equals(k))))                    break;                p = e;            }        }        if (e != null) { // existing mapping for key            V oldValue = e.value;            if (!onlyIfAbsent || oldValue == null)                e.value = value;            afterNodeAccess(e);            return oldValue;        }    }    ++modCount;    if (++size > threshold)        resize();    afterNodeInsertion(evict);    return null;}

 

可以看出,hashSet使用的是hashMap的put方法,而hashMap的put方法,使用hashCode()用key作為參數計算出hash值,然后進行比較,如果相同,再通過equals()比較key值是否相同,如果相同,返回同一個對象。

所以,如果類使用再散列表的集合對象中,要判斷兩個對象是否相同,除了要覆蓋equals()之外,也要覆蓋hashCode()函數。否則,equals()無效。

04、有哪些覆寫hashCode的訣竅

一個好的hashCode的方法的目標:為不相等的對象產生不相等的散列碼,同樣的,相等的對象必須擁有相等的散列碼。

 1、把某個非零的常數值,比如17,保存在一個int型的result中;

2、對於每個關鍵域f(equals方法中設計到的每個域),作以下操作:

  • a.為該域計算int類型的散列碼;

i.如果該域是boolean類型,則計算(f?1:0),ii.如果該域是byte,char,short或者int類型,計算(int)f,iii.如果是long類型,計算(int)(f^(f>>>32)).iv.如果是float類型,計算Float.floatToIntBits(f).v.如果是double類型,計算Double.doubleToLongBits(f),然后再計算long型的hash值vi.如果是對象引用,則遞歸的調用域的hashCode,如果是更復雜的比較,則需要為這個域計算一個范式,然后針對范式調用hashCode,如果為null,返回0vii. 如果是一個數組,則把每一個元素當成一個單獨的域來處理。
  • b.result = 31 * result + c;

3、返回result

4、編寫單元測試驗證有沒有實現所有相等的實例都有相等的散列碼。

給個簡單的例子:

@Overridepublic int hashCode() {  int result = 17;  result = 31 * result + name.hashCode();  return result;}

這里再說下2.b中為什么采用31*result + c,乘法使hash值依賴於域的順序,如果沒有乘法那么所有順序不同的字符串String對象都會有一樣的hash值,而31是一個奇素數,如果是偶數,並且乘法溢出的話,信息會丟失,31有個很好的特性是31*i ==(i<<5)-i,即2的5次方減1,虛擬機會優化乘法操作為移位操作的。


Java 極客技術公眾號,是由一群熱愛 Java 開發的技術人組建成立,專注分享原創、高質量的 Java 文章。如果您覺得我們的文章還不錯,請幫忙贊賞、在看、轉發支持,鼓勵我們分享出更好的文章。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM