有面試官會問:你重寫過 hashcode 和 equals 麼,為什麼重寫equals時必須重寫hashCode方法?equals和hashCode都是Object對象中的非final方法,它們設計的目的就是被用來覆寫(override)的,是以在程式設計中還是經常需要處理這兩個方法。下面我們一起來看一下,它們到底有什麼差別,總結一波!
01、hashCode介紹
hashCode() 的作用是擷取哈希碼,也稱為散列碼;它實際上是傳回一個int整數。這個哈希碼的作用是确定該對象在哈希表中的索引位置。hashCode() 定義在JDK的Object.java中,這就意味着Java中的任何類都包含有hashCode() 函數。
舉個例子
通過調用hashCode()方法擷取對象的hash值。
02、equals介紹
equals它的作用也是判斷兩個對象是否相等,如果對象重寫了equals()方法,比較兩個對象的内容是否相等;如果沒有重寫,比較兩個對象的位址是否相同,價于“==”。同樣的,equals()定義在JDK的Object.java中,這就意味着Java中的任何類都包含有equals()函數。
舉個例子
03、hashCode() 和 equals() 有什麼關系?
接下面,我們讨論另外一個話題。網上很多文章将 hashCode() 和 equals 關聯起來,有的講的不透徹,有誤導讀者的嫌疑。在這裡,我們梳理了一下 “hashCode() 和 equals()的關系”。我們以“類的用途”來将“hashCode() 和 equals()的關系”分2種情況來說明。
3.1、不會建立“類對應的散清單”
這裡所說的“不會建立類對應的散清單”是說:我們不會在HashSet, HashTable, HashMap等等這些本質是散清單的資料結構中,用到該類。例如,不會建立該類的HashSet集合。
在這種情況下,該類的“hashCode() 和 equals() ”沒有半毛錢關系的!
equals() 用來比較該類的兩個對象是否相等,而hashCode() 則根本沒有任何作用,是以,不用理會hashCode()。
舉個例子
運作結果:
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()方法。
運作結果:
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使用的是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中為什麼采用31result + c,乘法使hash值依賴于域的順序,如果沒有乘法那麼所有順序不同的字元串String對象都會有一樣的hash值,而31是一個奇素數,如果是偶數,并且乘法溢出的話,資訊會丢失,31有個很好的特性是31i ==(i<<5)-i,即2的5次方減1,虛拟機會優化乘法操作為移位操作的。