搞懂 Java equals 和 hashCode 方法

image

搞懂 Java equals 和 hashCode 方法

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

跟往常一樣,本文也盡可能結合面試題來重點講解下 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 函數(shù),比如需要使用 Map 來存放對象的時候,覆寫后 hashCode 就不是對象的內存地址了。

equals 詳解

equals 方法既然是基類 Object 的方法,我們創(chuàng)建的所有的對象都擁有這個方法,并有權利去重寫這個方法。該方法返回一個 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 數(shù)組中的每個元素是否相同
       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 方法已經(jīng)不單單是調用 this==obj來判斷對象是否相同了。事實上所有 Java 定義好的一些現(xiàn)有的引用數(shù)據(jù)類型都重寫了該方法。當我們自己定義引用數(shù)據(jù)類型的時候我們應該依照什么原則去判定兩個對象是否相同,這就需要我們自己來根據(jù)業(yè)務需求來把握。但是我們都需要遵循以下規(guī)則:

  • 自反性(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 的時候給新手還是帶來了蠻多困惑的。對于這兩個的區(qū)別需要看比較的對象是什么樣的類型。

我們都知道 Java 數(shù)據(jù)類型可分為 基本數(shù)據(jù)類型 和 引用數(shù)據(jù)類型?;緮?shù)據(jù)類型包括 byte, short, int , long , float , double , boolen ,char 八種。對于基本數(shù)據(jù)類型 == 操作符判斷的是左右兩邊變量的值:

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));

而對于引用數(shù)據(jù)類型 == 操作符判斷就是等號兩邊的指向的對象的內存地址是否相同。也就是說通過 == 判斷的兩個引用數(shù)據(jù)類型變量,如果相同,則他們指向的肯定是同一個對象。

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 與 == 操作符的區(qū)別總結如下:

  1. 若 == 兩側都是基本數(shù)據(jù)類型,則判斷的是左右兩邊操作數(shù)據(jù)的值是否相等

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

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

  4. equals 用于比較引用數(shù)據(jù)類型是否相等。在滿足equals 判斷規(guī)則的前體系,兩個對象只要規(guī)定的屬性相同我們就認為兩個對象是相同的。

hashCode 方法

hashCode 方法并沒有 equals 方法使用的那么頻繁,說道 hashCode 方法就不得不結合 Java 的 Map 容器,類似于 HashMap 這種使用了哈希算法容器會根據(jù)對象的hashCode返回值來初步確定對象在容器中的位置,然后內部再根據(jù)一定的 hash 算法來實現(xiàn)元素的存取。

hash 法簡介

hash 算法,又被成為散列算法,基本上,哈希算法就是將對象本身的鍵值,通過特定的數(shù)學函數(shù)運算或者使用其他方法,轉化成相應的數(shù)據(jù)存儲地址的。而哈希法所使用的數(shù)學函數(shù)就被稱為 『哈希函數(shù)』又可以稱之為散列函數(shù)。

說了這么多定義的東西,那這個 hash 算法究竟是干什么用的呢 ?我們可以通過一個例子來說明:

如果我們要在存放了的元素{0,4,6,9,28} 的數(shù)組中找到數(shù)值等于 6 的值的索引我們會怎么做?我們是不是需要遍歷一遍數(shù)組才能拿到對應的索引。在數(shù)組較大的時候這往往是低效率的。

如果我們能在數(shù)組存放的時候就按一定的規(guī)則放入元素,在我們想找某個元素的時候在根據(jù)之前定好的規(guī)則,就可以很快的得到我們想要的結果了。換句話說之前我們在數(shù)組中存放元素的順序可能是依照添加順序進行的,但是如果我們是按照一種既定的數(shù)學函數(shù)運算得到要放入元素的值,和數(shù)組角標的映射關系的話。那么我們在想取某個值的元素的時候就使用映射關系就可以找到對應的角標了。

在常見的 hash 函數(shù)中有一種最簡單的方法交「除留余數(shù)法」,操作方法就是將要存入數(shù)據(jù)除以某個常數(shù)后,使用余數(shù)作為索引值。 下面看個例子:

將 323 ,458 ,25 ,340 ,28 ,969, 77 使用「除留余數(shù)法」存儲在長度為11的數(shù)組中。我們假設上邊說的某個常數(shù)即為數(shù)組長度11。 每個數(shù)除以11以后存放的位置如下圖所示:

image

試想一下我們現(xiàn)在想要拿到 77 在數(shù)組中的位置,是不是只需要 arr[77%11] = 77 就可以了。

但是上述簡單的 hash 算法,缺點也是很明顯的,比如 77 和 88 對 11 取余數(shù)得到的值都是 0,但是角標為 0 位置已經(jīng)存放了 77 這個數(shù)據(jù),那88就不知道該去哪里了。上述現(xiàn)象在哈希法中有個名詞叫碰撞:

碰撞:若兩個不同的數(shù)據(jù)經(jīng)過相同哈希函數(shù)運算后,得到相同的結果,那么這種現(xiàn)象就做碰撞。

于是在設計 hash 函數(shù)的時候我們就要盡可能做到:

  1. 降低碰撞的可能性
  2. 盡量將要存入的元素經(jīng)過 hash 函數(shù)運算后的結果,盡量能夠均勻的分布在指定的容器(我們在稱之為桶)。

hashCode 方法 與 hash 算法的關系

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

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

前文說了 hashCode 方法與 java 中使用散列表的集合類息息相關,我們拿 Set 來舉例,我們都知道 Set 中是不允許存放重復的元素的。那么我們憑借什么來判斷已有的 Set 集合中是否有何要存入的元素重復的元素呢?有人可能會說我們可以通過 equals 來判斷兩個元素是否相同。那么問題又來,如果 Set 中已經(jīng)有 10000個元素了,那么之后在存入一個元素豈不是要調用 10000 次 equals 方法。顯然這不合理,性能低到令人發(fā)指。那要怎么辦才能保證即高效又不重復呢?答案就在于 hashCode 這個函數(shù)。

經(jīng)過之前的分析我們知道 hash 算法是使用特定的運算來得到數(shù)據(jù)的存儲位置的,那么 hashCode 方法就充當了這個特定的函數(shù)運算。這里我們可以簡單認為調用 hashCode 方法后得到數(shù)值就是元素的存儲位置(其實集合內部還做了進一步的運算,以保證盡可能的均勻分布在桶內)。

當 Set 需要存放一個元素的時候,首先會調用 hashCode 方法去查看對應的地址上有沒有存放元素,如果沒有則表示 Set 中肯定沒有相同的元素,直接存放在對應位置就好,但是如果 hashCode 的結果相同,即發(fā)生了碰撞,那么我們在進一步調用該位置元素的 equals 方法與要存放的元素進行比較,如果相同就不存了,如果不相同就需要進一步散列其它的地址。這樣我們就可以盡可能高效的保證了無重復元素的方法。

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

hashCode 和 equals 方法的關系

翻看Object 類對于 equals 方法的注釋上有這這么一條:

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

可以看到如果我們出于某種原因復寫了 equals 方法我們需要按照約定去覆寫 hashCode 方法,并且使用 equals 比較相同的對象,必須擁有相等的哈希碼。

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

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

結合 equals 方法的,我們可以做出如下總結:

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

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

我們先來看下第一個結論:調用 equals 返回 true 的兩個對象必須具有相等的哈希碼。為什么這么要求呢?比如我們還拿 Set 集合舉例,Set 首先會調用對象的 hashCode 方法尋找對象的存儲位置,如果兩個相同的對象調用 hashCode 方法得到的結果不同,那么造成的后果就是 Set 中存儲了相同的元素,而這樣的結果肯定是不對的。所以就要求 調用 equals 返回 true 的兩個對象必須具有相等的哈希碼

那么第二條為什么 hashCode 返回值相同,兩個對象卻不一定相同呢?這是因為,目前沒有完美的 hash 算法能夠完全的避免 「哈希碰撞」,既然碰撞是無法完全避免的所以兩個不相同的對象總有可能得到相同的哈希值。所以我們只能盡可能的保證不同的對象的 hashCode 不相同。事實上,對于 HashMap 在存儲鍵值對的時候,就會發(fā)生這樣的情況,在 JDK 1.7 之前,HashMap 對鍵的哈希值碰撞的處理方式,就是使用所謂的‘拉鏈法’。 具體實現(xiàn)會在之后分析 HashMap 的時候說到。

總結

本文總結了 equals 方法和 hashCode 方法的作用和意義。并學習了在覆寫這兩個方法的時候需要注意的要求。需要注意的是,關于這兩個方法在面試的時候還是很有可能被問及的所以,我們至少要明白:

  1. hashCode 返回值不一定對象的存儲地址,比如發(fā)生哈希碰撞的時候。
  2. 調用 equals 返回 true 的兩個對象必須具有相等的哈希碼。
  3. 如果兩個對象的 hashCode 返回值相同,調用它們 equals 方法不一返回 true 。
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容