最近這幾天一直對equals()和hashCode()的事搞不清楚,雲裏霧裏的。java
爲何重寫equals(),我知道。面試
可是爲何要兩個都要重寫呢,我就有點迷糊了,因此趁如今思考清楚後記錄一下。算法
經過本文,你能夠了解到數組
1.爲何要重寫equals(從普通角度而言)數據結構
2.爲何要重寫equals(從java數據結構角度而言)ide
3.爲何要重寫hashCode性能
4.哈希值與哈希表索引的關係學習
5.哈希衝突this
6.拉鍊法spa
7.HashSet和HashMap是怎麼添加元素的
原本我打算將源碼單獨寫一篇博文的,但想一想仍是在這裏寫算了。
無非就是一道面試題:「你重寫過 equals 和 hashcode 嗎,爲何重寫equals時必須重寫hashCode方法?」
也不用多說大道理了,咱們都知道Object類的equals()其實用的也是「==」。
咱們也知道「==」比較的是內存地址。
因此當對象同樣時,它的內存地址也是同樣的,因此此時不論是「==」也好,equals()也好,都是返回true。
例子
public static void main(String[] args) { String s = "大木大木大木大木"; String sb = s; System.out.println(s.equals(sb)); System.out.println(s == sb); }
輸出結果
true true
可是,咱們有時候判斷兩個對象是否相等不必定是要判斷它的內存地址是否相等,我只想根據對象的內容判斷。
在咱們人爲的規定下,我看到對象的內容相等,我就認爲這兩個對象是相等的,怎麼作呢?
很顯然,用「==」是作不到的,因此咱們須要用到equals()方法,
咱們須要重寫它,讓它達到咱們上面的目的,也就是根據對象內容判斷是否相等,而不是根據對象的內存地址。
例子:沒有重寫equls()
public class MyClass {
public static void main(String[] args) { Student s1 = new Student("jojo", 18); Student s2 = new Student("jojo", 18); System.out.println(s1.equals(s2)); } private static class Student { String name; int age; public Student(String name, int age) { this.name = name; this.age = age; } } }
輸出結果
false
結果分析
兩個長得同樣的對象比較,爲何equals()會返回false。
由於咱們的Student類沒有重寫equals()方法,因此它調用的實際上是Object類的equals(),其實現就是「==」。
因此雖然兩個對象長同樣,但它們的內存地址不同,兩個對象也就不相等,因此就返回了false。
因此咱們爲了達到咱們先前的規定,須要重寫一下equals()方法,至於重寫此方法,有幾點原則,我就很少說了,都是些廢話。
例子:重寫了equals()方法
private static class Student { String name; int age; public Student(String name, int age) { this.name = name; this.age = age; }
/**
* 重寫後的equals()是根據內容來斷定相等的
*/
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return age == student.age && name.equals(student.name); } }
此時再次執行如下代碼
public static void main(String[] args) { Student s1 = new Student("jojo", 18); Student s2 = new Student("jojo", 18); System.out.println(s1.equals(s2)); }
輸出結果
true
結果分析
顯然咱們此時已經達到了目的,根據內容可以斷定對象相等。
總結
1.Object類的equals()方法實現就是使用了"==",因此若是沒有重寫此方法,調用的依然是Object類的equals()
2.重寫equals()是爲了讓對象根據內容判斷是否相等,而不是內存地址。
關於hashCode(),百度百科是這麼描述的。
hashCode是jdk根據對象的地址或者字符串或者數字算出來的int類型的數值
詳細瞭解請 參考 public int hashCode()返回該對象的哈希值。
支持此方法是爲了提升哈希表(例如 java.util.Hashtable 提供的哈希表)的性能。
由於hashCode()的返回值就是一個哈希值。
因此下文沒有特別說明的話,文中的哈希值是指hashCode(),hashCode()一樣也能夠指哈希值。
哈希表的內容我這裏也很少提,但咱們須要明白的是
哈希值至關於一個元素在哈希表中的索引位置。
注意這裏是至關,而不是說確確切切就是 哈希值 = 索引 。
在java中,hashCode()返回的是int類型的一個哈希值。 int類型的範圍在不一樣編譯器有所不一樣,但也差不了什麼。 咱們以32位編譯器爲例,它的範圍是-2^31 到 2^31-1。 因此大概大概也都是四十多億個數了。 若是一個哈希值就是一個索引的話,那麼哈希表的長度就是四十多億,這明顯不可能。 一個哈希值使用一個索引,是行不通的,那就多個有相同特徵的哈希值使用一個索引。 因此爲了解決這個問題,能夠用什麼 除留取餘法 之類的,讓這些哈希值限定在一個索引範圍內。 這樣一來,哈希值的使用姑且解決了,但又出現一個致命的問題,那就是哈希衝突。 由於你這麼多哈希值都擠在一個範圍,確定會有出現一個索引多個哈希值的問題,這裏先拋開不講。
咱們回到最初的討論,從上面的話來看,哈希值才至關於索引,而不是確確切切等於索引。
由於真要講的話,不可能一臺機器上真就使用了那麼多內存,因此哈希衝突的機率很小。
哈希衝突少了,哈希值一人使用一個索引仍是綽綽有餘的,
其中還要知道,
元素的哈希值相同,內容不必定相同
元素的內容相同,哈希值必定相同。
爲何第一句會這麼說呢,由於其就是接下來所說的哈希衝突。
哈希衝突就是哈希值重複了,撞車了。
最直觀的見解是,兩個不一樣的元素,卻由於哈希算法不夠強,算出來的哈希值是同樣的。
因此解決哈希衝突的方法就是哈希算法儘量的強。
例子:弱的哈希算法
public class MyClass { public static void main(String[] args) { //Student對象 Student s1 = new Student("jojo", 18); Student s2 = new Student("JOJO", 18);//用equals()比較,並附帶hashCode() System.out.println("哈希值:s1-->" + s1.hashCode() + " s2-->" + s2.hashCode()); } private static class Student { String name; int age; public Student(String name, int age) { this.name = name; this.age = age; } /** * 此方法只是簡單的運算了name和age。 * @return */ @Override public int hashCode() { int nameHash = name.toUpperCase().hashCode(); return nameHash ^ age; } } }
輸出結果
哈希值:s1-->2282840 s2-->2282840
結果分析
咱們能夠看到這個兩個不一樣的對象卻由於簡單的哈希算法不夠健壯,致使了哈希值的重複。
這顯然不是咱們所但願的,因此能夠用更強的哈希算法。
例子:強的哈希算法
public class MyClass { public static void main(String[] args) { //Student對象 Student s1 = new Student("jojo", 18); Student s2 = new Student("JOJO", 18); //用equals()比較,並附帶hashCode() System.out.println("哈希值:s1-->" + s1.hashCode() + " s2-->" + s2.hashCode()); } private static class Student { String name; int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public int hashCode() { return Objects.hash(name, age); } } }
輸出結果
哈希值:s1-->101306313 s2-->70768585
結果分析
上面用的哈希算法是IDEA自動生成的,它是使用了java.util.Objects的hash()方法,
總而言之,好的哈希算法可以儘量的減小哈希衝突。
總結
哈希衝突能夠減小,但沒法避免,解決方法就是哈希算法儘量的強。
因此結合上面而言,咱們能夠認爲,哈希值越惟一越好,這樣在哈希表中插入對象時就不容易在同一個位置插入了。
可是,咱們但願哈希值惟一,現實卻不會如咱們但願,在哈希表中,哈希碼值的計算總會有撞車,有重複的,
關於哈希值的介紹僅此這麼點,更多詳情能夠繼續學習,這裏就很少說起了。
咱們上面說到由於多個哈希值使用同一個索引而引起哈希衝突,但哈希衝突不可避免,那就想辦法解決。
除了在哈希值方面下手,也能夠從存儲方面下手,好比待會要提的拉鍊法,由於在jdk1.8的HashMap就使用了這個。
拉鍊法我也不講那麼多官方話,直接看圖就懂。
拉鍊法就是這樣,索引相同的元素就用鏈表串起來。
不是全部重寫equals()的都要重寫hashCode(),若是不涉及到哈希表的話,就不用了,好比Student對象插入到List中。
涉及到哈希表,好比HashSet, Hashtable, HashMap這些數據結構,Student對象插入就必須考慮哈希值了。
如下的討論是設定在jdk1.8的HashSet,由於HashSet本質是哈希表的數據結構,是Set集合,是不容許有重複元素的。
它底層使用的是HashMap,因此也可說是討論HashMap,無差無差。
在這種狀況下才須要重寫equals()和重寫hashCode()。
咱們分四種狀況來講明。
例子
public class MyClass { public static void main(String[] args) { //Student對象 Student s1 = new Student("jojo", 18); Student s2 = new Student("jojo", 18); //HashSet對象 HashSet set = new HashSet(); set.add(s1); set.add(s2); //輸出兩個對象 System.out.println(s1); System.out.println(s2); //輸出equals System.out.println("s1.equals(s2)的結果爲:" + s1.equals(s2)); //輸出哈希值 System.out.println("哈希值爲:s1->" + s1.hashCode() + " s2->" + s2.hashCode()); //輸出set System.out.println(set); } private static class Student { String name; int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } } }
輸出結果
Student{name='jojo', age=18}
Student{name='jojo', age=18}
s1.equals(s2)的結果爲:false
哈希值爲:s1->2051450519 s2->99747242
[Student{name='jojo', age=18}, Student{name='jojo', age=18}]
結果分析
equals() 返回 false
哈希值 不一致
兩個都沒有重寫,HashSet將s1和s2都存進去了,明明算是重複元素,爲何呢?
到底HashSet怎麼纔算把兩個元素視爲重複呢?
咱們看源碼:
HashSet的add()調用了它底層的HashMap的put()。
而在HashMap的put()中又調用了putVal()。
當s1添加進來時,部分源碼爲
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 第一次添加元素,tab數組爲空,初始化數組 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // i爲索引,根據hash算出索引 // tab[i]此時爲null,因此s1就被插進到tab[i] if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
源碼中的hash已是被HashMap根據元素的hashCode()從新算出來的。
hash的題外話
咱們知道HashTable直接使用對象的hashCode(),而HashMap是從新計算哈希值。
但HashMap從新計算哈希值也是用到了元素的hashCode(),纔算出結果的。
因此總的來講hashCode()是惟一的變量,它跟hash是一一對應的。
再看s2添加進來時,部分源碼爲
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // tab數組已經初始過,跳過執行 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 此時由於沒有重寫hashCode(),因此s1和s2的哈希值是系統隨機算的,不一致不相同
// 因此根據哈希值算出來的hash也是新的 // 此時的i也是一個新的,s2插入tab[i] if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
因此第一種狀況的存儲狀況以下圖,固然,這是一種示意圖,並非真的索引,由於i也是算出來的。
存儲示意圖:
它們由於哈希值不一樣而直接被分配到兩個不一樣的位置裏了。
例子
public class MyClass { public static void main(String[] args) { //Student對象 Student s1 = new Student("jojo", 18); Student s2 = new Student("jojo", 18); //HashSet對象 HashSet set = new HashSet(); set.add(s1); set.add(s2); //輸出兩個對象 System.out.println(s1); System.out.println(s2); //輸出equals System.out.println("s1.equals(s2)的結果爲:" + s1.equals(s2)); //輸出哈希值 System.out.println("哈希值爲:s1->" + s1.hashCode() + " s2->" + s2.hashCode()); //輸出set System.out.println(set); } private static class Student { String name; int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return age == student.age && Objects.equals(name, student.name); } } }
輸出結果
Student{name='jojo', age=18} Student{name='jojo', age=18} s1.equals(s2)的結果爲:true 哈希值爲:s1->2051450519 s2->99747242 [Student{name='jojo', age=18}, Student{name='jojo', age=18}]
結果分析
equals() 返回 true
哈希值 不一致
重寫了equals()以後,HashSet依然將s1和s2都存進去了。
哈希值不一致,很明顯,從源碼看的話,依然跟狀況1如出一轍,
咱們看源碼:
不看了,跟狀況一的同樣,忘了的往上翻翻。
存儲示意圖:
也是可憐的小豬豬,哈希值不一樣,就天各一方。
例子
public class MyClass { public static void main(String[] args) { //Student對象 Student s1 = new Student("jojo", 18); Student s2 = new Student("jojo", 18); //HashSet對象 HashSet set = new HashSet(); set.add(s1); set.add(s2); //輸出兩個對象 System.out.println(s1); System.out.println(s2); //輸出equals System.out.println("s1.equals(s2)的結果爲:" + s1.equals(s2)); //輸出哈希值 System.out.println("哈希值爲:s1->" + s1.hashCode() + " s2->" + s2.hashCode()); //輸出set System.out.println(set); } private static class Student { String name; int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int hashCode() { return Objects.hash(name, age); } } }
輸出結果
Student{name='jojo', age=18} Student{name='jojo', age=18} s1.equals(s2)的結果爲:false 哈希值爲:s1->101306313 s2->101306313 [Student{name='jojo', age=18}, Student{name='jojo', age=18}]
結果分析
equals() 返回 false
哈希值 一致
此次咱們只重寫hashCode(),沒有重寫equals()。
此時哈希值一致了,可是依然存儲了兩個元素,讓咱們康康怎麼回事。
咱們看源碼:
當s1添加進來時,也是跟狀況一所說的同樣,tab數組初始化,放入tab[i]。
重點是s2的添加,s2的添加部分源碼爲
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // tab數組不爲空 跳過 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 此時重寫了hashCode(),獲得的哈希值跟元素內容相關,因此s1和s2的哈希值一致 // 再根據哈希值算出來的hash也一致,因此i也一致 // 按理說應該插入tab[i],但以前s1已經存在tab[i]了,因此就哈希衝突了,跳過執行 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { // 哈希衝突走這裏 Node<K,V> e; K k; // 再次判斷元素的hash和equals()以便確認是同一元素 // 但由於equals()沒有重寫,因此認爲s1和s2是兩個元素 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) { // 獲取到tab[i]上的鏈表結尾 if ((e = p.next) == null) { // 鏈表尾指向新節點,也就是指向s2 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) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
因此此時s2也被添加進去了。
存儲示意圖:
咱們能夠看到HashMap用了拉鍊法存儲s1和s2。
從這咱們能夠得知putVal()不只涉及了元素的哈希值,即hashCode(),還涉及到了equals()。
因此爲何上面只重寫hashCode()以後HashSet還能添加s1和s2,就是由於equals()沒有重寫。
致使了雖然哈希值相同,但equals()不一樣,因此認爲s1和s2是索引相同,內容不一樣的元素。
就將它們都插入了,而且插入的位置是同一個索引,也就是「拉鍊法」。
因此綜上所述,咱們能夠知道,對於這些哈希結構的東西,
它們判斷元素重複是先判斷哈希值而後再判斷equals()的。
也就是說
先判斷哈希值,若是哈希值相等,內容不必定等,此時繼續判斷equals()。
若是哈希值不等,那麼此時內容必定不等,就不用再判斷equals()了,直接操做。
例子
public class MyClass { public static void main(String[] args) { //Student對象 Student s1 = new Student("jojo", 18); Student s2 = new Student("jojo", 18); //HashSet對象 HashSet set = new HashSet(); set.add(s1); set.add(s2); //輸出兩個對象 System.out.println(s1); System.out.println(s2); //輸出equals System.out.println("s1.equals(s2)的結果爲:" + s1.equals(s2)); //輸出哈希值 System.out.println("哈希值爲:s1->" + s1.hashCode() + " s2->" + s2.hashCode()); //輸出set System.out.println(set); } private static class Student { String name; int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return age == student.age && Objects.equals(name, student.name); } @Override public int hashCode() { return Objects.hash(name, age); } } }
輸出結果
Student{name='jojo', age=18} Student{name='jojo', age=18} s1.equals(s2)的結果爲:true 哈希值爲:s1->101306313 s2->101306313 [Student{name='jojo', age=18}]
結果分析
equals() 返回 true
哈希值 一致
重寫了兩個方法後,equals()返回了true,哈希值也由於內容同樣而同樣,
更重要的是,HashSet只插入了一個元素。
咱們看源碼:
s1添加依然不用看了,都是同樣的,重點依然是s2的添加。
s2的添加前面跟第三種狀況的同樣,都是由於hash致使索引i相同,
而後須要用equals()加以判斷元素是否相同。
此時部分源碼爲
// 由於重寫有hashCode(),因此hash相同致使i相同 // 須要處理哈希衝突 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; // 經過hash和equals()判斷元素相同 // 而由於equals()重寫過了,因此經過 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 經過了 把p賦給e e = p; /******省略********/ } // 此時e不爲null,它等於p,p的key就是s1, // 在Hashset中,由於只有key,因此可將 <key,value> 當作 <key> // 因此此時能夠認爲e是s1 if (e != null) { // 記錄舊值 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) // 記錄新值 e.value = value; afterNodeAccess(e); // 返回舊值 return oldValue; }
從上面分析能夠看出,當經過hash和equals()判斷出s1和s2是同一個元素時,
直接就把p(也就是s1)賦給了e,徹底無論s2了。
因此能夠得知,元素相同時,
在HashMap中,會記錄舊value,更新value後返回舊value。
可是在HashSet中,由於只有key部分沒有value部分,因此返回的是一個空對象,
爲何是空對象呢。有代碼爲證。
// 這是HashSet的add,調用了HashMap的put() // put(K key,V value)是插入鍵值對 public boolean add(E e) { return map.put(e, PRESENT)==null; } // HashSet傳了一個key爲咱們add的元素,而value部分用的是它本身的東西 // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); //官方解釋說這個PRESENT就是一個虛擬值,說白了就是應付value用的
1.爲何要重寫equals()
從普通角度而言,重寫equals()是爲了讓兩個內容同樣的元素相等。
從java數據結構角度而言,哈希結構對元素的判斷跟哈希值以及equals()有關,因此必須重寫。
2.爲何要重寫hashCode()
重寫hashCode()是爲了讓哈希值跟元素內容產生關聯,從而保證了哈希值跟元素內容一一對應,
提升哈希值的惟一性,減小哈希衝突。
通常而言,equals()中用做比較的屬性,就是用來計算hashCode()的值,這樣才顯得關聯性和惟一性更高。
注:重寫hashCode()不是必須的,只有跟哈希結構有關時才須要重寫。
3.爲何重寫equals()時必須重寫hashCode()方法
其一
在跟哈希結構有關的狀況下,判斷元素重複是先判斷哈希值再判斷equals()。因此得兩個都重寫,
其二
重寫了hashCode()減小了哈希衝突,就能直接判斷元素的重複,而不用再繼續判斷equals(),從而提升了效率。
4.哈希值與哈希表索引的關係
在java中,哈希值,即(hashCode()返回值)是一個變量,雖然在HashMap中用的不是原生哈希值,而是重算了。
但重算的hash也是跟原生哈希值一一對應的,從而索引i也跟hash也是一一對應的。
因此 哈希值 跟 hash 一一對應, hash 跟 索引 一一對應,哈希值 跟 索引 一一對應。
三者雖然具體意義不一樣,但由於都是一一對應,因此能夠看作是同一個東西。
總之就是儘可能要惟一,惟一就完事了。
5.哈希衝突
哈希衝突是由於哈希值重複,解決哈希衝突有好幾個方法,拉鍊法只是其一,能夠自行學習。
可是最根本上而言,哈希衝突的源泉是哈希值重複,因此計算哈希值的時候算法越強就越少重複。
6.拉鍊法
我也不懂爲何叫拉鍊法,這個拉鍊跟圖的鄰接表類似,總的來講就是同一個索引上的元素串起來。
彩蛋:其實在jdk1.8的HashMap中,拉鍊法是有限的,一旦那條串串長度超過了限定長度,就會變爲紅黑樹。
7.HashSet和HashMap是怎麼添加元素的
由於HashSet主要是用HashMap實現的,因此能夠說HashMap是怎麼添加元素的
第一次添加,初始化存儲數組,直接插入對應索引的位置
之後添加,先根據hash算出索引,不衝突就直接插入
衝突的話,再根據hash和equals()判斷是否爲同一元素
是同一元素 記錄新值,返回舊值
不是同一元素 根據元素的特色類型是插入紅黑樹仍是鏈表(拉鍊法)