相信你面試的時候,確定被問過 hashCode 和 equals 相關的問題 。如:java
好的,上面就是靈魂拷問環節。其實,這些問題仔細想一下也不難,主要是平時咱們不多去思考它。面試
下面就按照上邊的問題順序,一個一個剖析它。扒開 hashCode 的神祕面紗。算法
咱們一般說的 hashCode 其實就是一個通過哈希運算以後的整型值。而這個哈希運算的算法,在 Object 類中就是經過一個本地方法 hashCode() 來實現的(HashMap 中還會有一些其它的運算)。緩存
public native int hashCode();
能夠看到它是一個本地方法。那麼,想要了解這個方法究竟是用來幹嗎的,最直接有效的方法就是,去看它的源碼註釋。ide
下邊我就用我蹩腳的英文翻譯一下它的意思。。。函數
返回當前對象的一個哈希值。這個方法用於支持一些哈希表,例如 HashMap 。源碼分析
一般來說,它有以下一些約定:性能
在實際狀況下,Object 類的 hashCode 方法在不一樣的對象中確實返回了不一樣的哈希值。這一般是經過把對象的內部地址轉換爲一個整數來實現的。學習
ps: 這裏說的內部地址就是指物理地址,也就是內存地址。須要注意的是,雖然 hashCode 值是依據它的內存地址而得來的。可是,不能說 hashCode 就表明對象的內存地址,實際上,hashCode 地址是存放在哈希表中的。this
上邊的源碼註釋真可謂是句句珠璣,把 hashCode 方法解釋的淋漓盡致。一下子我經過一個案例說明,就能明白我爲何這樣說了。
上文中提到了哈希表。什麼是哈希表呢?咱們直接看百度百科的解釋。
用一張圖來表示它們的關係。
左邊一列就是一些關鍵碼(key),經過哈希函數,它們都會獲得一個固定的值,分別對應右邊一列的某個值。右邊的這一列就能夠認爲是一張哈希表。
並且,咱們會發現,有可能有些 key 不一樣,可是它們對應的哈希值倒是同樣的,例如 aa,bb 都指向 1001 。可是,必定不會出現同一個 key 指向不一樣的值。
這也很是好理解,由於哈希表就是用來查找 key 的哈希地址的。在 key 肯定的狀況下,經過哈希函數計算出來的 哈希地址,必定也是肯定的。如圖中的 cc 已經肯定在 1002 位置了,那麼就不可能再佔據 1003 位置。
思考一下,若是有另一個元素 ee 來了,它的哈希地址也落在 1002 位置,怎麼辦呢?
其實,上圖就已經能夠說明一些問題了。咱們經過一個 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 底層實現原理及源碼分析
因此,hashCode 有什麼用。很明顯,提升了查詢,插入元素的效率呀。
這是萬年不變,經久不衰的經典面試題了。讓我油然想起,當初爲了面試,背誦過的面經了,簡直是一把心酸一把淚。如今還能記得這道題的標準答案:equals 比較的是內容, == 比較的是地址。
當時,真的就只是背答案,知其然而不知其因此然。再往下問,爲何要重寫 equals ,就懵逼了。
首先,咱們應該知道 equals 是定義在全部類的父類 Object 中的。
public boolean equals(Object obj) { return (this == obj); }
能夠看到,它的默認實現,就是 == ,這是用來比較內存地址的。因此,若是一個對象的 equals 不重寫的話,和 == 的效果是同樣的。
咱們知道,當建立兩個普通對象時,通常狀況下,它們所對應的內存地址是不同的。例如,我定義一個 User 類。
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 的方法,返回的確定是不相等的。所以,爲了更好地說明問題,我僅把第二行代碼修改以下:
//User user2 = new User("lisi", 18); User user2 = new User("zhangsan", 20);
讓 user1 和 user2 的內容相同,都是 zhangsan,20歲。按咱們的理解,這雖然是兩個對象,可是應該是指的同一我的,都是張三。可是,打印結果,以下:
這有悖於咱們的認知,明明是同一我的,爲何 equals 返回的卻不相等呢。所以,此時咱們就須要把 User 類中的 equals 方法重寫,以達到咱們的目的。在 User 中添加以下代碼(使用 idea 自動生成代碼):
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 。由於,在對象內容相同的狀況下,咱們須要讓對象相等。所以,不能用 Object 類的默認實現,只去比較內存地址,這樣是不合理的。
那 hashCode 爲何要重寫呢? 這就涉及到集合,如 Map 和 Set (底層其實也是 Map)了。
咱們以 HashMap JDK1.8的源碼來看,如 put 方法。
咱們會發現,代碼中會屢次進行 hash 值的比較,只有當哈希值相等時,纔會去比較 equals 方法。當 hashCode 和 equals 都相同時,纔會覆蓋元素。get 方法也是如此(先比較哈希值,再比較equals),
只有 hashCode 和 equals 都相等時,才認爲是同一個元素,找到並返回此元素,不然返回 null。
這也對應 「hashCode 有什麼用?」這一小節。 重寫 equals 和 hashCode 的目的,就是爲了方便哈希表這樣的結構快速的查詢和插入。若是不重寫,則沒法比較元素,甚至形成元素位置錯亂。
答案是確定的。首先,在上邊的 JDK 源碼註釋中第第二點,咱們就會發現這句說明。其次,咱們嘗試重寫 equals ,而不重寫 hashCode 看會發生什麼現象。
public class TestHashCode { public static void main(String[] args) { User user1 = new User("zhangsan", 20); User user2 = new User("zhangsan", 20); HashMap<User, Integer> map = new HashMap<>(); map.put(user1,90); System.out.println(map.get(user2)); } } // 打印結果: null
對於代碼中的 user1 和 user2 兩個對象來講,咱們認爲他是同一我的張三。定義一個 map ,key 存儲 User 對象, value 存儲他的學習成績。
當把 user1 對象做爲 key ,成績 90 做爲 value 存儲到 map 中時,咱們確定但願,用 key 爲 user2 來取值時,獲得的結果是 90 。可是,結果卻大失所望,獲得了 null 。
這是由於,咱們自定義的 User 類,雖然重寫了 equals ,可是沒有重寫 hashCode 。當 user1 放到 map 中時,計算出來的哈希值和用 user2 去取值時計算的哈希值不相等。所以,equals 方法都沒有比較的機會。認爲他們是不一樣的元素。然而,其實,咱們應該認爲 user1 和 user2 是相同的元素的。
用圖來講明就是,user1 和 user2 存放在了 HashMap 中不一樣的桶裏邊,致使查詢不到目標元素。
所以,當咱們用自定義類來做爲 HashMap 的 key 時,必需要重寫 hashCode 和 equals 。不然,會獲得咱們不想要的結果。
這也是爲何,咱們平時都喜歡用 String 字符串來做爲 key 的緣由。 由於, String 類默認就幫咱們實現了 equals 和 hashCode 方法的重寫。以下,
// String.java public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; //從前向後依次比較字符串中的每一個字符 if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; } public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; //把字符串中的每一個字符都取出來,參與運算 for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } //把計算出來的最終值,存放在hash變量中。 hash = h; } return h; }
重寫 equals 時,可使用 idea 提供的自動代碼,也能夠本身手動實現。
public class User { ... //省略已知代碼 @Override public int hashCode() { return Objects.hash(name, age); } } //此時,map.get(user2) 能夠獲得 90 的正確值
在重寫了 hashCode 後,使用自定義對象做爲 key 時,還須要注意一點,不要在使用過程當中,改變對象的內容,這樣會致使 hashCode 值發生改變,一樣得不到正確的結果。以下,
public class TestHashCode { public static void main(String[] args) { User user = new User("zhangsan", 20); HashMap<User, Integer> map = new HashMap<>(); map.put(user,90); System.out.println(map.get(user)); user.setAge(18); //把對象的年齡修改成18 System.out.println(map.get(user)); } } // 打印結果: // 90 // null
會發現,修改後,拿到的值是 null 。這也是,hashCode 源碼註釋中的第一點說明的,hashCode 值不變的前提是,對象的信息沒有被修改。若被修改,則有可能致使 hashCode 值改變。
此時,有沒有聯想到其餘一些問題。好比,爲何 String 類要設計成不能夠變的呢?這裏用 String 做爲 HashMap 的 key 時,能夠算做一個緣由。你確定不但願,放進去的時候還好好的,取出來的時候,卻找不到元素了吧。
String 類內部會有一個變量(hash)來緩存字符串的 hashCode 值。只有字符串不可變,才能夠保證哈希值不變。
很顯然不是的。在 HashMap 的源碼中,咱們就能看到,當 hashCode 相等時(產生哈希碰撞),還須要比較它們的 equals ,才能夠肯定是不是同一個對象。所以,hashCode 相等時, equals 不必定相等 。
反過來,equals 相等的話, hashCode 必定相等嗎? 那必須的。equals 都相等了,那說明在 HashMap 中認爲它們是同一個元素,因此 hashCode 值必須也要保證相等。
結論:
關於最後這一點,就是 hashCode 源碼註釋中提到的第三點。當 equals 不等時,不用必須保證它們的 hashCode 也不相等。可是爲了提升哈希表的效率,最好設計成不等。
由於,咱們既然知道它們不相等了,那麼當 hashCode 設計成不等時。只要比較 hashCode 不相等,咱們就能夠直接返回 null,而沒必要再去比較 equals 了。這樣,就減小了比較的次數,無疑提升了效率。
以上就是 hashCode 和 equals 相關的一些問題。相信已經能夠解答你心中的疑惑了,也能夠和麪試官侃侃而談。不再用擔憂,面試官說換人了。