1. 程式人生 > >面試官:換人!他連雜湊扣的都不懂

面試官:換人!他連雜湊扣的都不懂

## 前言 相信你面試的時候,肯定被問過 hashCode 和 equals 相關的問題 。如: * hashCode 是什麼?它是怎麼得來的?有什麼用? * 經典題,equals 和 == 有什麼區別? * 為什麼要重寫 equals 和 hashCode ? * 重寫了 equals ,就必須要重寫 hashCode 嗎?為什麼? * hashCode 相等時,equals 一定相等嗎?反過來呢? 好的,上面就是靈魂拷問環節。其實,這些問題仔細想一下也不難,主要是平時我們很少去思考它。 ## 正文 下面就按照上邊的問題順序,一個一個剖析它。扒開 hashCode 的神祕面紗。 ### 什麼是 hashCode? 我們通常說的 hashCode 其實就是一個經過雜湊運算之後的整型值。而這個雜湊運算的演算法,在 Object 類中就是通過一個本地方法 hashCode() 來實現的(HashMap 中還會有一些其它的運算)。 ```java public native int hashCode(); ``` 可以看到它是一個本地方法。那麼,想要了解這個方法到底是用來幹嘛的,最直接有效的方法就是,去看它的原始碼註釋。 ![](https://img2020.cnblogs.com/other/1714084/202006/1714084-20200623200105707-975510350.png) 下邊我就用我蹩腳的英文翻譯一下它的意思。。。 返回當前物件的一個雜湊值。這個方法用於支援一些雜湊表,例如 HashMap 。 通常來講,它有如下一些約定: * 若物件的資訊沒有被修改,那麼,在一個程式的執行期間,對於相同的物件,不管呼叫多少次 hashCode 方法,都應該返回相同的值。當然,在相同程式的不同執行期間,不需要保持結果一致。 * 若兩個物件的 equals 方法返回值相同,那麼,呼叫它們各自的 hashCode 方法時,也必須返回相同的結果。(ps: 這句話解答了上邊的一些問題,後面會用例子來證明這一點) * 當兩個物件的 equals 方法返回值不同時,那麼它們的 hashCode 方法不用保證必須返回不同的值。但是,我們應該知道,在這種情況下,我們最好也設計成 hashCode 返回不同的值。因為,這樣做有助於提高雜湊表的效能。 在實際情況下,Object 類的 hashCode 方法在不同的物件中確實返回了不同的雜湊值。這通常是通過把物件的內部地址轉換為一個整數來實現的。 ps: 這裡說的內部地址就是指實體地址,也就是記憶體地址。需要注意的是,雖然 hashCode 值是依據它的記憶體地址而得來的。但是,不能說 hashCode 就代表物件的記憶體地址,實際上,hashCode 地址是存放在雜湊表中的。 上邊的原始碼註釋真可謂是句句珠璣,把 hashCode 方法解釋的淋漓盡致。一會兒我通過一個案例說明,就能明白我為什麼這樣說了。 ### 什麼是雜湊表? 上文中提到了雜湊表。什麼是雜湊表呢?我們直接看百度百科的解釋。 ![](https://img2020.cnblogs.com/other/1714084/202006/1714084-20200623200105950-93972745.png) 用一張圖來表示它們的關係。 ![](https://img2020.cnblogs.com/other/1714084/202006/1714084-20200623200106180-1481286726.png) 左邊一列就是一些關鍵碼(key),通過雜湊函式,它們都會得到一個固定的值,分別對應右邊一列的某個值。右邊的這一列就可以認為是一張雜湊表。 而且,我們會發現,有可能有些 key 不同,但是它們對應的雜湊值卻是一樣的,例如 aa,bb 都指向 1001 。但是,一定不會出現同一個 key 指向不同的值。 這也非常好理解,因為雜湊表就是用來查詢 key 的雜湊地址的。在 key 確定的情況下,通過雜湊函式計算出來的 雜湊地址,一定也是確定的。如圖中的 cc 已經確定在 1002 位置了,那麼就不可能再佔據 1003 位置。 思考一下,如果有另外一個元素 ee 來了,它的雜湊地址也落在 1002 位置,怎麼辦呢? ### hashCode 有什麼用? 其實,上圖就已經可以說明一些問題了。我們通過一個 key 計算出它的 hashCode 值,就可以唯一確定它在雜湊表中的位置。這樣,在查詢時,就可以直接定位到當前元素,提高查詢效率。 現在我們假設有這樣一個場景。我們需要在記憶體中的一塊兒區域存放 10000 個不同的元素(以aa,bb,cc,dd 等為例)。那怎麼實現不同的元素插入,相同的元素覆蓋呢? 我們最容易想到的方法就是,每當存一個新元素時,就遍歷一遍已經存在的元素,看有沒有相同的。這樣雖然也是可以實現的,但是,如果已經存在了 9000 個元素,你就需要去遍歷一下這 9000 個元素。很明顯,這樣的效率是非常低下的。 我們轉換一種思路,還是以上圖為例。若來了一個新元素 ff,首先去計算它的 hashCode 值,得出為 1003 。發現此處還沒有元素,則直接把這個新元素 ff 放到此位置。 然後,ee 來了,通過計算雜湊值得到 1002 。此時,發現 1002 位置已經存在一個元素了。那麼,通過 equals 方法比較它們是否相等,發現只有一個 dd 元素,很明顯和 ee 不相等。那麼,就把 ee 元素放到 dd 元素的後邊(可以用連結串列形式存放)。 我們會發現,當有新元素來的時候,先去計算它們的雜湊值,再去確定存放的位置,這樣就可以減少比較的次數。如 ff 不需要比較, ee 只需要和 dd 比較一次。 當元素越來越多的時候,新元素也只需要和當前雜湊值相同的位置上,已經存在的元素進行比較。而不需要和其他雜湊值不同的位置上的元素進行比較。這樣就大大減少了元素的比較次數。 圖中為了方便,畫的雜湊表比較小。現在假設,這個雜湊表非常的大,例如有這麼非常多個位置,從 1001 ~ 9999。那麼,新元素插入的時候,有很大概率會插入到一個還沒有元素存在的位置上,這樣就不需要比較了,效率非常高。但是,我們會發現這樣也有一個弊端,就是雜湊表所佔的記憶體空間就會變大。因此,這是一個權衡的過程。 有心的同學可能已經發現了。我去,上邊的這個做法好熟悉啊。沒錯,它就是大名鼎鼎的 HashMap 底層實現的思想。對 HashMap 還不瞭解的,趕緊看這篇文章理一下思路。[HashMap 底層實現原理及原始碼分析](https://blog.csdn.net/qq_26542493/article/details/105482732) 所以,hashCode 有什麼用。很明顯,提高了查詢,插入元素的效率呀。 ### equals 和 == 有什麼區別? 這是萬年不變,經久不衰的經典面試題了。讓我油然想起,當初為了面試,背誦過的面經了,簡直是一把心酸一把淚。現在還能記得這道題的標準答案:equals 比較的是內容, == 比較的是地址。 當時,真的就只是背答案,知其然而不知其所以然。再往下問,為什麼要重寫 equals ,就懵逼了。 首先,我們應該知道 equals 是定義在所有類的父類 Object 中的。 ```java public boolean equals(Object obj) { return (this == obj); } ``` 可以看到,它的預設實現,就是 == ,這是用來比較記憶體地址的。所以,如果一個物件的 equals 不重寫的話,和 == 的效果是一樣的。 我們知道,當建立兩個普通物件時,一般情況下,它們所對應的記憶體地址是不一樣的。例如,我定義一個 User 類。 ```java public class User { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public User(String name, int age) { this.name = name; this.age = age; } public User() { } } public class TestHashCode { public static void main(String[] args) { User user1 = new User("zhangsan", 20); User user2 = new User("lisi", 18); System.out.println(user1 == user2); System.out.println(user1.equals(user2)); } } // 結果: false false ``` 很明顯,zhangsan 和 lisi 是兩個人,兩個不同的物件。因此,它們所對應的記憶體地址不同,而且內容也不相等。 注意,這裡我還沒有對 User 重寫 equals,實際此時 equals 使用的是父類 Object 的方法,返回的肯定是不相等的。因此,為了更好地說明問題,我僅把第二行程式碼修改如下: ```java //User user2 = new User("lisi", 18); User user2 = new User("zhangsan", 20); ``` 讓 user1 和 user2 的內容相同,都是 zhangsan,20歲。按我們的理解,這雖然是兩個物件,但是應該是指的同一個人,都是張三。但是,列印結果,如下: ![](https://img2020.cnblogs.com/other/1714084/202006/1714084-20200623200106377-254776053.png) 這有悖於我們的認知,明明是同一個人,為什麼 equals 返回的卻不相等呢。因此,此時我們就需要把 User 類中的 equals 方法重寫,以達到我們的目的。在 User 中新增如下程式碼(使用 idea 自動生成程式碼): ```java public class User { ... //省略已知程式碼 @Override public boolean equals(Object o) { //若兩個物件的記憶體地址相同,則說明指向的是同一個物件,故內容一定相同。 if (this == o) return true; //類都不是同一個,更別談相等了 if (o == null || getClass() != o.getClass()) return false; User user = (User) o; //比較兩個物件中的所有屬性,即name和age都必須相同,才可認為兩個物件相等 return age == user.age && Objects.equals(name, user.name); } } //列印結果: false true ``` 再次執行程式,我們會發現此時 equals 返回 true ,這才是我們想要的。 因此,當我們使用自定義物件時。如果需要讓兩個物件的內容相同時,equals 返回 true,則需要重寫 equals 方法。 ### 為什麼要重寫 equals 和 hashCode ? 在上邊的案例中,其實我們已經說明了為什麼要去重寫 equals 。因為,在物件內容相同的情況下,我們需要讓物件相等。因此,不能用 Object 類的預設實現,只去比較記憶體地址,這樣是不合理的。 那 hashCode 為什麼要重寫呢? 這就涉及到集合,如 Map 和 Set (底層其實也是 Map)了。 我們以 HashMap JDK1.8的原始碼來看,如 put 方法。 ![](https://img2020.cnblogs.com/other/1714084/202006/1714084-20200623200106684-475569231.png) 我們會發現,程式碼中會多次進行 hash 值的比較,只有當雜湊值相等時,才會去比較 equals 方法。當 hashCode 和 equals 都相同時,才會覆蓋元素。get 方法也是如此(先比較雜湊值,再比較equals), ![](https://img2020.cnblogs.com/other/1714084/202006/1714084-20200623200106971-1289645846.png) 只有 hashCode 和 equals 都相等時,才認為是同一個元素,找到並返回此元素,否則返回 null。 這也對應 “hashCode 有什麼用?”這一小節。 重寫 equals 和 hashCode 的目的,就是為了方便雜湊表這樣的結構快速的查詢和插入。如果不重寫,則無法比較元素,甚至造成元素位置錯亂。 ### 重寫了 equals ,就必須要重寫 hashCode 嗎? 答案是肯定的。首先,在上邊的 JDK 原始碼註釋中第第二點,我們就會發現這句說明。其次,我們嘗試重寫 equals ,而不重寫 hashCode 看會發生什麼現象。 ```java public class TestHashCode { public static void main(String[] args) { User user1 = new User("zhangsan", 20); User user2 = new User("zhangsan", 20);