面試官:換人!他連雜湊扣的都不懂
阿新 • • 發佈:2020-06-23
## 前言
相信你面試的時候,肯定被問過 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);