Java 中 hashCode() 和 equals() 的關(guān)系是面試中的??键c,如果沒有深入思考過兩者設(shè)計的初衷,這個問題將很難回答。除了應(yīng)付面試,理解二者的關(guān)系更有助于我們寫出高質(zhì)量且準確的代碼。 一.基礎(chǔ):hashCode() 和 equals() 簡介
equals()equals() 方法用于比較兩個對象是否相等,它與 == 相等比較符有著本質(zhì)的不同。 在萬物皆對象的 Java 體系中,系統(tǒng)把判斷對象是否相等的權(quán)力交給程序員。具體的措施是把 equals() 方法寫到 Object 類中,并讓所有類繼承 Object 類。這樣程序員就能在自定義的類中重寫 equals() 方法, 從而實現(xiàn)自己的比較邏輯。 hashCode()hashCode() 的意思是哈希值, 哈希值是經(jīng)哈希函數(shù)運算后得到的結(jié)果,哈希函數(shù)能夠保證相同的輸入能夠得到相同的輸出(哈希值),但是不能夠保證不同的輸入總是能得出不同的輸出。 當輸入的樣本量足夠大時,是會產(chǎn)生哈希沖突的,也就是說不同的輸入產(chǎn)生了相同的輸出。 暫且不談沖突,就相同的輸入能夠產(chǎn)生相同的輸出這點而言,是及其寶貴的。它使得系統(tǒng)只需要通過簡單的運算,在時間復(fù)雜度O(1)的情況下就能得出數(shù)據(jù)的映射關(guān)系,根據(jù)這種特性,散列表應(yīng)運而生。 一種主流的散列表實現(xiàn)是:用數(shù)組作為哈希函數(shù)的輸出域,輸入值經(jīng)過哈希函數(shù)計算后得到哈希值。然后根據(jù)哈希值,在數(shù)組種找到對應(yīng)的存儲單元。當發(fā)生沖突時,對應(yīng)的存儲單元以鏈表的形式保存沖突的數(shù)據(jù)。 二. 漫談:初識 hashCode() 與 equals() 之間的關(guān)系
在大多數(shù)編程實踐中,歸根結(jié)底會落實到數(shù)據(jù)的存取問題上。在匯編語言時代,你需要老老實實地對每個數(shù)據(jù)操作編寫存取語句。 而隨著時代發(fā)展到今天,我們都用更方便靈活的高級語言編寫代碼,比如 Java。 Java 以面向?qū)ο鬄楹诵乃枷耄庋b了一系列操作數(shù)據(jù)的 api,降低了數(shù)據(jù)操作的復(fù)雜度。 但在我們對數(shù)據(jù)進行操作之前,首先要把數(shù)據(jù)按照一定的數(shù)據(jù)結(jié)構(gòu)保存到存儲單元中,否則操作數(shù)據(jù)將無從談起。 然而不同的數(shù)據(jù)結(jié)構(gòu)有各自的特點,我們在存儲數(shù)據(jù)的時候需要選擇合適的數(shù)據(jù)結(jié)構(gòu)進行存儲。Java 根據(jù)不同的數(shù)據(jù)結(jié)構(gòu)提供了豐富的容器類,方便程序員選擇適合業(yè)務(wù)的容器類進行開發(fā)。 通過繼承關(guān)系圖我們看到 Java 的容器類被分為 Collection 和 Map 兩大類,Collection 又可以進一步分為 List 和 Set。 其中 Map 和 Set 都是不允許元素重復(fù)的,嚴格來說Map存儲的是鍵值對,它不允許重復(fù)的鍵值。 值得注意的是:Map 和 Set 的絕大多數(shù)實現(xiàn)類的底層都會用到散列表結(jié)構(gòu)。 講到這里我們提取兩個關(guān)鍵字不允許重復(fù)和散列表結(jié)構(gòu),回顧 hashCode() 和 equals() 的特點,你是否想到了些什么東西呢? 三. 解密:深入理解 hashCode() 和 equals() 之間的關(guān)系equals() 會有力不從心的時候上面提到 Set 和 Map 不存放重復(fù)的元素(key),這些容器在存儲元素的時必須對元素做出判斷:在當前的容器中有沒有和新元素相同的元素? 你可能會想:這容易呀,直接調(diào)用元素對象的 equals() 方法進行比較不就行了嗎? 如果容器中的存儲的對象數(shù)量較少,這確實是個好主意,但是如果容器中存放的對象達到了一定的規(guī)模,要調(diào)用容器中所有對象的 equals() 方法和新元素進行比較,就不是一件容易的事情了。 就算 equals() 方法的比較邏輯簡單無比,總的來說也是一個時間復(fù)雜度為 O(n) 的操作啊。 hashCode() 小力出奇跡但在散列表的基礎(chǔ)上,判斷“新對象是否和已存在對象相同”就容易得多了。 由于每個對象都自帶有 hashCode(),這個 hashCode 將會用作散列表哈希函數(shù)的輸入,hashCode 經(jīng)過哈希函數(shù)計算后得到哈希值,新對象會根據(jù)哈希值,存儲到相應(yīng)的內(nèi)存的單元。 我們不妨假設(shè)兩個相同的對象,hashCode() 一定相同,這么一來就體現(xiàn)出哈希函數(shù)的威力了。 由于相同的輸入一定會產(chǎn)生相同的輸出,于是如果新對象,和容器中已存在的對象相同,新對象計算出的哈希值就會和已存在的對象的哈希值產(chǎn)生沖突。 這時容器就能判斷:這個新加入的元素已經(jīng)存在,需要另作處理:覆蓋掉原來的元素(key)或舍棄。 按照這個思路,如果這個元素計算出的哈希值所對應(yīng)的內(nèi)存單元沒有產(chǎn)生沖突,也就是沒有重復(fù)的元素,那么它就可以直接插入。 所以當運用 hashCode() 時,判斷是否有相同元素的代價,只是一次哈希計算,時間復(fù)雜度為O(1),這極大地提高了數(shù)據(jù)的存儲性能。 Java 設(shè)計 equals(),hashCode() 時約定的規(guī)則前面我們還提到:當輸入樣本量足夠大時,不相同的輸入是會產(chǎn)生相同輸出的,也就是形成哈希沖突。 這么一來就麻煩了,原來我們設(shè)定的“如果產(chǎn)生沖突,就意味著兩個對象相同”的規(guī)則瞬間被打破,因為產(chǎn)生沖突的很有可能是兩個不同的對象! 而令人欣慰的是我們除了 hashCode() 方法,還有一張王牌:equals() 方法。 也就是說當兩個不相同的對象產(chǎn)生哈希沖突后,我們可以用 equals() 方法進一步判斷兩個對象是否相同。 這時 equals() 方法就相當重要了,這個情況下它必須要能判定這兩個對象是不相同的。
如果兩個對象是相等的,它們的 equals() 方法應(yīng)該要返回 true,它們的 hashCode() 需要返回相同的結(jié)果。 但有時候面試不會問得這么直接,他會問你:兩個對象的 hashCdoe() 相同,它的 equals() 方法一定要返回 true,對嗎? 那答案肯定不對。因為我們不能保證每個程序設(shè)計者,都會遵循編碼約定。 有可能兩個不同對象的hashCode()會返回相同的結(jié)果,但是由于他們是不同的對象,他們的 equals() 方法會返回false。 如果你理解上面的內(nèi)容,這個問題就很好解答,我們再回顧一下: 如果兩個對象的 hashCode() 相同,將來就會在散列表中產(chǎn)生哈希沖突,但是它們不一定是相同的對象呀。 當產(chǎn)生哈希沖突時,我們還得通過 equals() 方法進一步判斷兩個對象是否相同,equals() 方法不一定會返回 true。 這也是為什么 Java 官方推薦我們在一個類中,最好同時重寫 hashCode() 和 equals() 方法的原因。 四. 驗證:結(jié)合 HashMap 的源碼和官方文檔,驗證兩者的關(guān)系
通過閱讀JDK8的官方文檔,我們發(fā)現(xiàn) equals() 方法介紹的最后有這么一段話:
官方文檔提醒我們當重寫 equals() 方法的時候,最好也要重寫 hashCode() 方法。 也就是說如果我們通過重寫 equals() 方法判斷兩個對象相同時,他們的hash code也應(yīng)該相同,這樣才能讓hashCode()方法發(fā)揮它的作用。 那它究竟能發(fā)會怎樣的作用呢? 我們結(jié)合部分較為常用的 HashMap 源碼進一步分析。(像 HashSet 底層也是通過 HashMap 實現(xiàn)的) 在 HashMap 中用得最多無疑是 put() 方法了,以下是put()的源碼:
我們可以看到 put() 方法實際調(diào)用的是 putVal() 方法,繼續(xù)跟進:
我們可以看到每當判斷 key 是否相同時,首先會判斷 hash 值,如果 hash 值相同(產(chǎn)生了沖突),然后會判斷 key 引用所指的對象是否相同,最終會通過 equals() 方法作最后的判定。 如果 key 的 hash 值不同,后面的判斷將不會執(zhí)行,直接認定兩個對象不相同。
五. 結(jié)束講到這里希望大家對 hashCode() 與 equals() 方法能有更深入的理解,明白背后的設(shè)計思想與原理。 我之前有一個疑問,可能大家看完這篇文章后也會有:equals() 方法平時我會用到,所以我知道它除了和 hashCode() 方法有密切聯(lián)系外,還有別的用途。 但是hashCode()呢?它除了和equals()方法有密切聯(lián)系外,還有其他用途嗎? 經(jīng)過在互聯(lián)網(wǎng)上一番搜尋,我目前給出的答案是沒有。 也就是說 hashCode() 僅在散列表中才有用,在其它情況下沒用。 |
|
來自: 一本正經(jīng)地胡鬧 > 《計算機》