搞懂 Java equals 和 hashCode 方法

搞懂 Java equals 和 hashCode 方法

分析完 Java List 容器的源碼後,原本想直接進入 Set 和 Map 容器的源碼分析,可是對於這兩種容器,內部存儲元素的方式的都是以鍵值對相關的,而元素如何存放,便與 equalshashCode 這兩個方法密切相關。因此在分析 Map 家族以前,須要深刻了解下這兩個方法,並且這兩個方法在面試的時候也屬於極有可能考察的問題。java

跟往常同樣,本文也儘量結合面試題來重點講解下 equals 和 hashCode 的使用以及意義。程序員

概述

首先 equalshashCode 兩個方法屬於 Object 基類的方法:面試

public boolean equals(Object obj) {
   return (this == obj);
}

public native int hashCode();

複製代碼

能夠看出 equals 方法默認比較的是兩個對象的引用是否指向同一個內存地址。而 hashCode 這是一個 native 本地方法,其實默認的 hashCode 方法返回的就是對象對應的內存地址。算法

hasCode 方法的註釋這樣說的: This is typically implemented by converting the internal address of the object into an integer,數組

這一點咱們經過 toString 方法也能夠間接瞭解,咱們都知道 toString 返回的是「類名@十六進制內存地址」,由源碼能夠看出內存地址與 hashCode() 返回值相同。函數

public String toString() {
   return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
複製代碼

面試題目: hashCode 方法返回的是對象的內存地址麼? 答: Object 基類的 hashCode 方法默認返回對象的內存地址,可是在一些場景下咱們須要覆寫 hashCode 函數,好比須要使用 Map 來存放對象的時候,覆寫後 hashCode 就不是對象的內存地址了。源碼分析

equals 詳解

equals 方法既然是基類 Object 的方法,咱們建立的全部的對象都擁有這個方法,並有權利去重寫這個方法。該方法返回一個 boolean 值,表明比較的兩個對象是否相同,這裏的相同的條件由重寫 equals 方法的類來解決。好比咱們都知道 :性能

String str1 = "abc";
String str2 = "abc";
str1.equals(str2);//true
複製代碼

顯然 String 類必定重寫了 equals 方法不然兩個 String 對象內存地址確定不一樣。咱們簡單看下 String 類的 equals 方法:學習

public boolean equals(Object anObject) {
   //首先判斷兩個對象的內存地址是否相同
   if (this == anObject) {
       return true;
   }
   // 判斷連個對象是否屬於同一類型。
   if (anObject instanceof String) {
       String anotherString = (String)anObject;
       int n = value.length;
       //長度相同的狀況下逐一比較 char 數組中的每一個元素是否相同
       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;
}
複製代碼

從源碼咱們也能夠看出, equals 方法已經不僅僅是調用 this==obj來判斷對象是否相同了。事實上全部 Java 定義好的一些現有的引用數據類型都重寫了該方法。當咱們本身定義引用數據類型的時候咱們應該依照什麼原則去斷定兩個對象是否相同,這就須要咱們本身來根據業務需求來把握。可是咱們都須要遵循如下規則:flex

  • 自反性(reflexive)。對於任意不爲 null 的引用值 x,x.equals(x) 必定是 true。

  • 對稱性(symmetric)。對於任意不爲 null 的引用值 x 和 y ,當且僅當x.equals(y)是 true 時,y.equals(x)也是true。

  • 傳遞性(transitive)。對於任意不爲 null 的引用值x、y和z,若是 x.equals(y) 是 true,同時 y.equals(z) 是 true,那麼x.equals(z)必定是 true。

  • 一致性(consistent)。對於任意不爲null的引用值x和y,若是用於equals比較的對象信息沒有被修改的話,屢次調用時 x.equals(y) 要麼一致地返回 true 要麼一致地返回 false。

  • 對於任意不爲 null 的引用值 x,x.equals(null) 返回 false。

equals vs ==

說到 equals 怎麼能不說 == ,其實兩個在初學 Java 的時候給新手仍是帶來了蠻多困惑的。對於這兩個的區別須要看比較的對象是什麼樣的類型。

咱們都知道 Java 數據類型可分爲 基本數據類型 和 引用數據類型。基本數據類型包括 byte, short, int , long , float , double , boolen ,char 八種。對於基本數據類型 == 操做符判斷的是左右兩邊變量的值:

int a = 10;
int b = 10;
float c = 10.0f;
//如下輸出結果均爲 true
System.out.println("(a == b) = " + (a == b));
System.out.println("(b == c) = " + (b == c));
複製代碼

而對於引用數據類型 == 操做符判斷就是等號兩邊的指向的對象的內存地址是否相同。也就是說經過 == 判斷的兩個引用數據類型變量,若是相同,則他們指向的確定是同一個對象。

EntryClass entryClass1 = new EntryClass(1);
EntryClass entryClass2 = new EntryClass(1);
EntryClass entryClass3 = entryClass1;
 
 // (entryClass1 == entryClass2) = false   
System.out.println(" (entryClass1 == entryClass2) = " + (entryClass1 == entryClass2));
// (entryClass1 == entryClass3) = true
System.out.println(" (entryClass1 == entryClass3) = " + (entryClass1 == entryClass3));
複製代碼

equals 與 == 操做符的區別總結以下:

  1. 若 == 兩側都是基本數據類型,則判斷的是左右兩邊操做數據的值是否相等

  2. 若 == 兩側都是引用數據類型,則判斷的是左右兩邊操做數的內存地址是否相同。若此時返回 true , 則該操做符做用的必定是同一個對象。

  3. Object 基類的 equals 默認比較兩個對象的內存地址,在構建的對象沒有重寫 equals 方法的時候,與 == 操做符比較的結果相同。

  4. equals 用於比較引用數據類型是否相等。在知足equals 判斷規則的前體系,兩個對象只要規定的屬性相同咱們就認爲兩個對象是相同的。

hashCode 方法

hashCode 方法並無 equals 方法使用的那麼頻繁,說道 hashCode 方法就不得不結合 Java 的 Map 容器,相似於 HashMap 這種使用了哈希算法容器會根據對象的hashCode返回值來初步肯定對象在容器中的位置,而後內部再根據必定的 hash 算法來實現元素的存取。

hash 法簡介

hash 算法,又被成爲散列算法,基本上,哈希算法就是將對象自己的鍵值,經過特定的數學函數運算或者使用其餘方法,轉化成相應的數據存儲地址的。而哈希法所使用的數學函數就被稱爲 『哈希函數』又能夠稱之爲散列函數。

說了這麼多定義的東西,那這個 hash 算法到底是幹什麼用的呢 ?咱們能夠經過一個例子來講明:

若是咱們要在存放了的元素{0,4,6,9,28} 的數組中找到數值等於 6 的值的索引咱們會怎麼作?咱們是否是須要遍歷一遍數組才能拿到對應的索引。在數組較大的時候這每每是低效率的。

若是咱們能在數組存放的時候就按必定的規則放入元素,在咱們想找某個元素的時候在根據以前定好的規則,就能夠很快的獲得咱們想要的結果了。換句話說以前咱們在數組中存放元素的順序多是依照添加順序進行的,可是若是咱們是按照一種既定的數學函數運算獲得要放入元素的值,和數組角標的映射關係的話。那麼咱們在想取某個值的元素的時候就使用映射關係就能夠找到對應的角標了。

在常見的 hash 函數中有一種最簡單的方法交「除留餘數法」,操做方法就是將要存入數據除以某個常數後,使用餘數做爲索引值。 下面看個例子:

將 323 ,458 ,25 ,340 ,28 ,969, 77 使用「除留餘數法」存儲在長度爲11的數組中。咱們假設上邊說的某個常數即爲數組長度11。 每一個數除以11之後存放的位置以下圖所示:

 

 

試想一下咱們如今想要拿到 77 在數組中的位置,是否是隻須要 arr[77%11] = 77 就能夠了。

可是上述簡單的 hash 算法,缺點也是很明顯的,好比 77 和 88 對 11 取餘數獲得的值都是 0,可是角標爲 0 位置已經存放了 77 這個數據,那88就不知道該去哪裏了。上述現象在哈希法中有個名詞叫碰撞:

碰撞:若兩個不一樣的數據通過相同哈希函數運算後,獲得相同的結果,那麼這種現象就作碰撞。

因而在設計 hash 函數的時候咱們就要儘量作到:

  1. 下降碰撞的可能性
  2. 儘可能將要存入的元素通過 hash 函數運算後的結果,儘可能可以均勻的分佈在指定的容器(咱們在稱之爲桶)。

hashCode 方法 與 hash 算法的關係

其實 Java 中的有所的對象又擁有 hashCode 方法其實就是一種 hash 算法,只是有的類覆寫好提供給咱們了,有些就須要咱們手動去覆寫。好比咱們能夠看一下 String 提供給咱們的 hashCode 算法:

public int hashCode() {
   int h = hash;//默認是0
   if (h == 0 && value.length > 0) {
       char val[] = value;
        // 字符串轉化的 char 數組中每個元素都參與運算
       for (int i = 0; i < value.length; i++) {
           h = 31 * h + val[i];
       }
       hash = h;
   }
   return h;
}
複製代碼

前文說了 hashCode 方法與 java 中使用散列表的集合類息息相關,咱們拿 Set 來舉例,咱們都知道 Set 中是不容許存放重複的元素的。那麼咱們憑藉什麼來判斷已有的 Set 集合中是否有何要存入的元素重複的元素呢?有人可能會說咱們能夠經過 equals 來判斷兩個元素是否相同。那麼問題又來,若是 Set 中已經有 10000個元素了,那麼以後在存入一個元素豈不是要調用 10000 次 equals 方法。顯然這不合理,性能低到使人髮指。那要怎麼辦才能保證即高效又不重複呢?答案就在於 hashCode 這個函數。

通過以前的分析咱們知道 hash 算法是使用特定的運算來獲得數據的存儲位置的,那麼 hashCode 方法就充當了這個特定的函數運算。這裏咱們能夠簡單認爲調用 hashCode 方法後獲得數值就是元素的存儲位置(其實集合內部還作了進一步的運算,以保證儘量的均勻分佈在桶內)。

當 Set 須要存放一個元素的時候,首先會調用 hashCode 方法去查看對應的地址上有沒有存放元素,若是沒有則表示 Set 中確定沒有相同的元素,直接存放在對應位置就好,可是若是 hashCode 的結果相同,即發生了碰撞,那麼咱們在進一步調用該位置元素的 equals 方法與要存放的元素進行比較,若是相同就不存了,若是不相同就須要進一步散列其它的地址。這樣咱們就能夠儘量高效的保證了無重複元素的方法。

面試題: hashCode 方法的做用和意義 答: 在 Java 中 hashCode 的存在主要是用於提升容器查找和存儲的快捷性,如 HashSet, Hashtable,HashMap 等,hashCode是用來在散列存儲結構中肯定對象的存儲地址的,

hashCode 和 equals 方法的關係

翻看Object 類對於 equals 方法的註釋上有這這麼一條:

請注意,當這個方法被重寫時,一般須要覆蓋{@code hashCode}方法,以便維護{@code hashCode}方法的通常契約,該方法聲明相等對象必須具備相等的哈希碼.

能夠看到若是咱們出於某種緣由複寫了 equals 方法咱們須要按照約定去覆寫 hashCode 方法,而且使用 equals 比較相同的對象,必須擁有相等的哈希碼。

Object 對於 hashCode 方法也有幾條要求:

  1. 在 Java 應用程序執行期間,在對同一對象屢次調用 hashCode 方法時,必須一致地返回相同的整數,前提是將對象進行 equals 比較時所用的信息沒有被修改。從某一應用程序的一次執行到同一應用程序的另外一次執行,該整數無需保持一致。
  2. 若是根據 equals(Object) 方法,兩個對象是相等的,那麼對這兩個對象中的每一個對象調用 hashCode 方法都必須生成相同的整數結果。
  1. 若是根據 equals(java.lang.Object) 方法,兩個對象不相等,那麼對這兩個對象中的任一對象上調用 hashCode 方法 不要求 必定生成不一樣的整數結果。可是,程序員應該意識到,爲不相等的對象生成不一樣整數結果能夠提升哈希表的性能。  

結合 equals 方法的,咱們能夠作出以下總結:

  1. 調用 equals 返回 true 的兩個對象必須具備相等的哈希碼。

  2. 若是兩個對象的 hashCode 返回值相同,調用它們 equals 方法不一返回 true 。

咱們先來看下第一個結論:調用 equals 返回 true 的兩個對象必須具備相等的哈希碼。爲何這麼要求呢?好比咱們還拿 Set 集合舉例,Set 首先會調用對象的 hashCode 方法尋找對象的存儲位置,若是兩個相同的對象調用 hashCode 方法獲得的結果不一樣,那麼形成的後果就是 Set 中存儲了相同的元素,而這樣的結果確定是不對的。因此就要求 調用 equals 返回 true 的兩個對象必須具備相等的哈希碼

那麼第二條爲何 hashCode 返回值相同,兩個對象卻不必定相同呢?這是由於,目前沒有完美的 hash 算法可以徹底的避免 「哈希碰撞」,既然碰撞是沒法徹底避免的因此兩個不相同的對象總有可能獲得相同的哈希值。因此咱們只能儘量的保證不一樣的對象的 hashCode 不相同。事實上,對於 HashMap 在存儲鍵值對的時候,就會發生這樣的狀況,在 JDK 1.7 以前,HashMap 對鍵的哈希值碰撞的處理方式,就是使用所謂的‘拉鍊法’。 具體實現會在以後分析 HashMap 的時候說到。

總結

本文總結了 equals 方法和 hashCode 方法的做用和意義。並學習了在覆寫這兩個方法的時候須要注意的要求。須要注意的是,關於這兩個方法在面試的時候仍是頗有可能被問及的因此,咱們至少要明白:

  1. hashCode 返回值不必定對象的存儲地址,好比發生哈希碰撞的時候。
  2. 調用 equals 返回 true 的兩個對象必須具備相等的哈希碼。
  3. 若是兩個對象的 hashCode 返回值相同,調用它們 equals 方法不一返回 true 。
相關文章
相關標籤/搜索