搞懂 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 函數,比如需要使用 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 定義好的一些現有的引用數據類型都重寫了該方法。當我們自己定義引用數據類型的時候我們應該依照什么原則去判定兩個對象是否相同,這就需要我們自己來根據業務需求來把握。但是我們都需要遵循以下規則:

  • 自反性(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以后存放的位置如下圖所示:

image

試想一下我們現在想要拿到 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 。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容