1. 程式人生 > >面試點:Java 中 hashCode() 和 equals() 的關係

面試點:Java 中 hashCode() 和 equals() 的關係

Java 中 hashCode() 和 equals() 的關係是面試中的常考點,如果沒有深入思考過兩者設計的初衷,這個問題將很難回答。除了應付面試,理解二者的關係更有助於我們寫出高質量且準確的程式碼。

一.基礎:hashCode() 和 equals() 簡介

在學習 hashCode() 和 equals() 之間的關係之前, 我們有必要先單獨地瞭解他倆的特點.

equals()

equals() 方法用於比較兩個物件是否相等,它與 == 相等比較符有著本質的不同。

在萬物皆物件的 Java 體系中,系統把判斷物件是否相等的權力交給程式設計師。具體的措施是把 equals() 方法寫到 Object 類中,並讓所有類繼承 Object 類。這樣程式設計師就能在自定義的類中重寫 equals() 方法, 從而實現自己的比較邏輯。

hashCode()

hashCode() 的意思是雜湊值, 雜湊值是經雜湊函式運算後得到的結果,雜湊函式能夠保證相同的輸入能夠得到相同的輸出(雜湊值),但是不能夠保證不同的輸入總是能得出不同的輸出。

當輸入的樣本量足夠大時,是會產生雜湊衝突的,也就是說不同的輸入產生了相同的輸出。

暫且不談衝突,就相同的輸入能夠產生相同的輸出這點而言,是及其寶貴的。它使得系統只需要通過簡單的運算,在時間複雜度O(1)的情況下就能得出資料的對映關係,根據這種特性,散列表應運而生。

一種主流的散列表實現是:用陣列作為雜湊函式的輸出域,輸入值經過雜湊函式計算後得到雜湊值。然後根據雜湊值,在陣列種找到對應的儲存單元。當發生衝突時,對應的儲存單元以連結串列的形式儲存衝突的資料。

二. 漫談:初識 hashCode() 與 equals() 之間的關係

下面我們從一個巨集觀的角度討論 hashCode() 和 equals() 之間的關係。

在大多數程式設計實踐中,歸根結底會落實到資料的存取問題上。在組合語言時代,你需要老老實實地對每個資料操作編寫存取語句。

而隨著時代發展到今天,我們都用更方便靈活的高階語言編寫程式碼,比如 Java。

Java 以面向物件為核心思想,封裝了一系列操作資料的 api,降低了資料操作的複雜度。

但在我們對資料進行操作之前,首先要把資料按照一定的資料結構儲存到儲存單元中,否則操作資料將無從談起。

然而不同的資料結構有各自的特點,我們在儲存資料的時候需要選擇合適的資料結構進行儲存。Java 根據不同的資料結構提供了豐富的容器類,方便程式設計師選擇適合業務的容器類進行開發。

通過繼承關係圖我們看到 Java 的容器類被分為 Collection 和 Map 兩大類,Collection 又可以進一步分為 List 和 Set。 其中 Map 和 Set 都是不允許元素重複的,嚴格來說Map儲存的是鍵值對,它不允許重複的鍵值。

值得注意的是:Map 和 Set 的絕大多數實現類的底層都會用到散列表結構。

講到這裡我們提取兩個關鍵字不允許重複散列表結構,回顧 hashCode() 和 equals() 的特點,你是否想到了些什麼東西呢?

三. 解密:深入理解 hashCode() 和 equals() 之間的關係

equals() 會有力不從心的時候

上面提到 Set 和 Map 不存放重複的元素(key),這些容器在儲存元素的時必須對元素做出判斷:在當前的容器中有沒有和新元素相同的元素

你可能會想:這容易呀,直接呼叫元素物件的 equals() 方法進行比較不就行了嗎?

如果容器中的儲存的物件數量較少,這確實是個好主意,但是如果容器中存放的物件達到了一定的規模,要呼叫容器中所有物件的 equals() 方法和新元素進行比較,就不是一件容易的事情了。

就算 equals() 方法的比較邏輯簡單無比,總的來說也是一個時間複雜度為 O(n) 的操作啊。

hashCode() 小力出奇跡

但在散列表的基礎上,判斷“新物件是否和已存在物件相同”就容易得多了。

由於每個物件都自帶有 hashCode(),這個 hashCode 將會用作散列表雜湊函式的輸入,hashCode 經過雜湊函式計算後得到雜湊值,新物件會根據雜湊值,儲存到相應的記憶體的單元。

我們不妨假設兩個相同的物件,hashCode() 一定相同,這麼一來就體現出雜湊函式的威力了。

由於相同的輸入一定會產生相同的輸出,於是如果新物件,和容器中已存在的物件相同,新物件計算出的雜湊值就會和已存在的物件的雜湊值產生衝突。

這時容器就能判斷:這個新加入的元素已經存在,需要另作處理:覆蓋掉原來的元素(key)或捨棄。

按照這個思路,如果這個元素計算出的雜湊值所對應的記憶體單元沒有產生衝突,也就是沒有重複的元素,那麼它就可以直接插入。

所以當運用 hashCode() 時,判斷是否有相同元素的代價,只是一次雜湊計算,時間複雜度為O(1),這極大地提高了資料的儲存效能。

Java 設計 equals(),hashCode() 時約定的規則

前面我們還提到:當輸入樣本量足夠大時,不相同的輸入是會產生相同輸出的,也就是形成雜湊衝突。

這麼一來就麻煩了,原來我們設定的“如果產生衝突,就意味著兩個物件相同”的規則瞬間被打破,因為產生衝突的很有可能是兩個不同的物件!

而令人欣慰的是我們除了 hashCode() 方法,還有一張王牌:equals() 方法。

也就是說當兩個不相同的物件產生雜湊衝突後,我們可以用 equals() 方法進一步判斷兩個物件是否相同。

這時 equals() 方法就相當重要了,這個情況下它必須要能判定這兩個物件是不相同的。

  • 講到這裡就引出了 Java 程式設計中一個重要原則: 如果兩個物件是相等的,它們的 equals() 方法應該要返回 true,它們的 hashCode() 需要返回相同的結果

但有時候面試不會問得這麼直接,他會問你:兩個物件的 hashCdoe() 相同,它的 equals() 方法一定要返回 true,對嗎

那答案肯定不對。因為我們不能保證每個程式設計者,都會遵循編碼約定。

有可能兩個不同物件的hashCode()會返回相同的結果,但是由於他們是不同的物件,他們的 equals() 方法會返回false。

如果你理解上面的內容,這個問題就很好解答,我們再回顧一下:

如果兩個物件的 hashCode() 相同,將來就會在散列表中產生雜湊衝突,但是它們不一定是相同的物件呀。

當產生雜湊衝突時,我們還得通過 equals() 方法進一步判斷兩個物件是否相同,equals() 方法不一定會返回 true。

這也是為什麼 Java 官方推薦我們在一個類中,最好同時重寫 hashCode() 和 equals() 方法的原因。

四. 驗證:結合 HashMap 的原始碼和官方文件,驗證兩者的關係

以上的文字,是我經過思考後得出的,它有一定依據但並非完全可靠。下面我們根據 HashMap 的原始碼(JDK1.8)和官方文件,來驗證這些推論是否正確。

通過閱讀JDK8的官方文件,我們發現 equals() 方法介紹的最後有這麼一段話:

Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.

官方文件提醒我們當重寫 equals() 方法的時候,最好也要重寫 hashCode() 方法。

也就是說如果我們通過重寫 equals() 方法判斷兩個物件相同時,他們的hash code也應該相同,這樣才能讓hashCode()方法發揮它的作用。

那它究竟能發會怎樣的作用呢?

我們結合部分較為常用的 HashMap 原始碼進一步分析。(像 HashSet 底層也是通過 HashMap 實現的)

在 HashMap 中用得最多無疑是 put() 方法了,以下是put()的原始碼:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

我們可以看到 put() 方法實際呼叫的是 putVal() 方法,繼續跟進:

final V putVal(int hash, K key, V value, Boolean onlyIfAbsent,
               Boolean evict) {
	Node<K,V>[] tab;
	Node<K,V> p;
	int n, i;
	//在我們建立HashMap物件的時候, 記憶體中並沒有為HashMap分配表的空間, 直到往HashMap中put新增元素的時候才呼叫resize()方法初始化表
	if ((tab = table) == null || (n = tab.length) == 0)
	        n = (tab = resize()).length;
	//同時確定了表的長度
	//((n - 1) & hash)確定了要put的元素的位置, 如果要插入的地方是空的, 就可以直接插入.
	if ((p = tab[i = (n - 1) & hash]) == null)
	        tab[i] = newNode(hash, key, value, null); else {
		//如果發生了衝突, 就要在衝突位置的連結串列末尾插入元素
		Node<K,V> e;
		K k;
		if (p.hash == hash &&   
		            ((k = p.key) == key || (key != null && key.equals(k))))
		            //關鍵!!!當判斷新加入的元素是否與已有的元素相同, 首先判斷的是hash值, 後面再呼叫equals()方法. 如果hash值不同是直接跳過的
		e = p; else if (p instanceof TreeNode)//如果衝突解決方案已經變成紅黑樹的話, 按紅黑樹的策略新增結點. 
		e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {
			//解決衝突的方式仍是連結串列
			for (int binCount = 0; ; ++binCount) {
				//找到連結串列的末尾, 插入.
				if ((e = p.next) == null) {
					p.next = newNode(hash, key, value, null);
					if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
					treeifyBin(tab, hash);
					//插入之後要判斷連結串列的長度, 如果到達一定的值就可能要轉換為紅黑樹. 
					break;
				}
				//在遍歷的過程中仍會不停地判定當前key是否與傳入的key相同, 判斷的第一條件仍然是hash值. 
				if (e.hash == hash &&
				                    ((k = e.key) == key || (key != null && key.equals(k))))
				                    break;
				p = e;
			}
		}
		if (e != null) {
			// existing mapping for key
			V oldValue = e.value;
			if (!onlyIfAbsent || oldValue == null)
			                e.value = value;
			afterNodeAccess(e);
			return oldValue;
		}
	}
	++modCount;
	//修改map的次數增加
	if (++size > threshold)//如果hashMap的容量到達了一定值就要進行擴容
	resize();
	afterNodeInsertion(evict);
	return null;
}

我們可以看到每當判斷 key 是否相同時,首先會判斷 hash 值,如果 hash 值相同(產生了衝突),然後會判斷 key 引用所指的物件是否相同,最終會通過 equals() 方法作最後的判定。

如果 key 的 hash 值不同,後面的判斷將不會執行,直接認定兩個物件不相同。

if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
    e = p;

五. 結束

講到這裡希望大家對 hashCode() 與 equals() 方法能有更深入的理解,明白背後的設計思想與原理。

我之前有一個疑問,可能大家看完這篇文章後也會有:equals() 方法平時我會用到,所以我知道它除了和 hashCode() 方法有密切聯絡外,還有別的用途。

但是hashCode()呢?它除了和equals()方法有密切聯絡外,還有其他用途嗎?

經過在網際網路上一番搜尋,我目前給出的答案是沒有。

也就是說 hashCode() 僅在散列表中才有用,在其它情況下沒用。

當然如果這個答案不正確,或者你還有別的思考,歡迎留言與我交流~

寫在最後

  • 第一:看完點贊,感謝您對作者的認可;
  • ...
  • 第二:隨手轉發,分享知識,讓更多人學習到;
  • ...
  • 第三:記得點關注,每天更新的!!!
  • ...

相關推薦

試點Java hashCode() equals() 的關係

Java 中 hashCode() 和 equals() 的關係是面試中的常考點,如果沒有深入思考過兩者設計的初衷,這個問題將很難

javahashCodeequals什麽關系,hashCode到底怎麽用的

true private ech return 運行 我們 load mark == Object類的hashCode的用法:(新手一定要忽略本節,否則會很慘) (視頻下載) (全部書籍)馬 克-to-win:hashCode方法主要是Sun編寫的一些數據結構比如Hasht

Java hashCode()equals()方法

Java中,涉及到兩個物件的比較時,我們會用到hashCode()和equals()。這兩個方法是Object類中定義的方法。 1. api中的描述 (1)hashCode() hashCode()方法給物件返回一個hash code值。這個方法被用於hash tables,

從語言設計的角度探究JavahashCode()equals()的關係

目錄 一. 基礎: hashCode()和equals()簡介 二. 漫談: 引入hashCode()與equals()之間的關係 三. 解密: 深入理解hashCode()和equals()之間的關係. 四

Java的==equals

基於 equals return 復合 覆蓋 之間 方法 [] 。。 1.基本數據類型   byte ,short ,int ,long ,double ,float,boolean,char   他們之間的比較,應用雙等號(==),比較的是他們的值。 2.復合數據類型(類

JavahashCodeequals¥==的區別

類重寫 對象 相同 判斷 RR over bool == hashtable ref:http://www.cnblogs.com/skywang12345/p/3324958.html 1、==作用:   java中的==用來判斷兩個對象的地址是否相等;當對象是基本數據類

Java重寫hashCode()equals()方法

哈希 strong tag main 實現 sta 位置 rgs out 1. hashCode 1.1 基本概念   hashCode是JDK根據對象的地址算出來的一個int數字(對象的哈希碼值),代表了該對象再內存中的存儲位置。   hashCode()方法是超級類Ob

javahashCode()equals()方法的探討

我們經常聽說過如果我們重寫的equals()方法,那麼我們必須重新hashCode()方法,那麼為什麼要這麼做呢,其實其中還是有原因的 我們可以舉一個例子,可以利用HashSet來檢驗,我們知道HashSet中定義了一個HashMap的變數和一個final型別的Object

java試題java的單例設計模式及兩種實現方法的程式碼舉例

java面試時經常會問到關於單例設計模式,因為它能考察的知識點較多且在開發中經常用到。那我就來說一說我對於單例設計模式的一些淺見。首先,在Java中,什麼是單例呢?就是保證類在記憶體中只有一個物件。那麼

javahashcodeequals詳解(集合的用法)

一:Java中的equals方法和hashCode方法是Object中的,所以每個物件都是有這兩個方法的,有時候我們需要實現特定需求,可能要重寫這兩個方法 equals()和hashCode()方法是用來在同一類中做比較用的,尤其是在容器裡如set存放同一類物件

Object類hashCode()equals()方法詳解(附圖)

 下圖是規範中要求的: 圖解:比如equals相等的箭頭指向hashcode相等,標示equals相等那麼必有hashcode相等。另外有兩個箭頭指向別人的標示可能是其中之一。 //JAVA程式碼: public static void main

試題Java物件序列化介面(Serializable)的意義

Serializable介面是一個裡面什麼都沒有的介面 它的原始碼是public interface Serializable{},即什麼都沒有。 如果一個接口裡面什麼內容都沒有,那麼這個介面是一個標識介面,比如,一個學生遇到一個問題,排錯排了幾天也沒解決,此時,她舉手了(示意我去幫他解決),然後我過去,幫他

hashsethashcodeequals方法

equals和hashcode 在hashset中,加入元素先判斷兩個物件的hashcode是否相等,在判斷equals是否相等,以為equals效率低,用hashcode判斷可以減少equals的呼叫次數,增加效率。物件的equals方法如果沒重寫預設繼承object,o

Java集合試題(02) JavaListSet之間區別

本文為本博主翻譯,未經允許,嚴禁轉載! 簡介 Java中List和Set之間有什麼區別是一個非常流行的Java集合面試問題,也是在Java中使用Collection類時要記住的一個重要的基本概念。 List和Set都是Java程式最重要的Collection類中的兩個,以及

JavahashCodeequals方法的約定及重寫原則

Java中Set的contains()方法 —— hashCode與equals方法的約定及重寫原則翻譯人員: 鐵錨翻譯時間: 2013年11月5日原文連結: Java hashCode() and equals() Contract for the contains(Obj

聽說你還搞不定java的==equals

相信很多讀者關於==和equals懂了又懵,懵了又懂,如此迴圈,事實上可能是因為看到的部落格文章之類的太多了,長篇大論,加上一段時間的洗禮之後就迷路了。本篇文章再一次理清楚。當然如果覺得本文太囉嗦的話,當然我也考慮到了,因為我也不喜歡長篇大論囉裡囉嗦比比叨叨胡攪蠻纏的文章,畢竟大家入門java 的時候就知道個

java面試(1) java==equalshashCode的區別

1."=="     "=="運算子是比較兩個變數的值是否相等。也就是說,該運算子用於比較變數對應的記憶體中所儲存的值是否相等,要比較兩個基礎型別的資料或兩個引用變數是否相等,只能使用"=="運算子。     具體而言,如果兩個變數是基礎

理解JavahashCodeequals 方法

err array size tex nat 什麽 map 交流群 培訓 在Java裏面所有的類都直接或者間接的繼承了java.lang.Object類,Object類裏面提供了11個方法,如下: Java代碼 ```` 1,clone() 2,equals(Obje

試題思考java快速失敗(fail-fast)安全失敗(fail-safe)的區別是什麽?

無效對象 ring list 改變 ava ret fail last 原理 一:快速失敗(fail—fast) 在用叠代器遍歷一個集合對象時,如果遍歷過程中對集合對象的內容進行了修改(增加、刪除、修改),則會拋出Concurrent Modificat

java正確使用hashCodeequals方法

Java 中正確使用 hashCode 和 equals 方法 轉載自:[開源中國社](http://www.oschina.net/question/82993_75533) 在這篇文章中,我將告訴大家我對hashCo