關注公眾號,大家可以在公眾號后台回復“博客園”,免費獲得作者 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,返回0
vii. 如果是一個數組,則把每一個元素當成一個單獨的域來處理。
-
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 文章。如果您覺得我們的文章還不錯,請幫忙贊賞、在看、轉發支持,鼓勵我們分享出更好的文章。