從語言設計的角度探究Java中hashCode()和equals()的關係

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

在學習hashCode()和equals()之間的關係以前, 咱們有必要先單獨瞭解他倆自身的特色.java

  • equals()方法用於比較兩個對象是否相等, 它與"=="相等比較符有着本質的不一樣. 在萬物皆對象的Java體系中, 系統把判斷對象是否相等的權力交給程序員, 具體的措施是把equals()方法寫入到Object類中, 並讓全部類繼承Object類. 這樣程序員就能在類中自定義equals()方法, 從而實現本身的業務邏輯.
  • 關於equals()和"=="的區別你能夠--參考這篇文章--

 

  • 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()之間的關係.

  • 上面提到Set和Map不存放重複的元素(key), 那麼在存儲元素的時候就必須對元素作出判斷: 在當前的容器中有沒有和新元素相同的元素?.
  • 你可能會想: 這容易呀, 直接調用元素對象的equals()方法進行比較不就好了嗎? 若是容器中的存儲的對象數量較少, 這確實是個好主意, 可是若是容器中存放的對象達到了必定的規模, 要調用容器中全部對象的equals()方法和新元素進行比較就不是一件容易的事情了, 就算equals()方法的比較邏輯簡單無比, 這也是一個時間複雜度爲O(n)的操做啊.

 

  • 但在散列表的基礎上判斷"新對象是否和容器中任一對象相同"就容易得多了. 因爲每一個對象都自帶有hashCode(), 這個hashCode將會用做散列表哈希函數的輸入, hashCode通過哈希函數計算後獲得哈希值, 新對象根據哈希值存儲到相應的內存的單元.
  • 咱們不妨假設兩個相同的對象, hashCode()必定相同, 這麼一來就體現出哈希函數的威力了, 因爲相同的輸入必定會產生相同的輸出, 因而若是新對象和容器中已存在的對象相同, 新對象計算出的哈希值就會和已存在的對象的哈希值產生衝突, 這時容器就能判斷: 這個新加入的元素已經存在, 須要另做處理(覆蓋掉原來的元素(key)或捨棄).
  • 按照這個思路, 若是這個元素計算出的哈希值所對應的地址沒有產生衝突, 也就是沒有重複的元素, 那麼它就能夠直接插入. 因此當運用hashCode()時, 判斷是否有相同元素的代價只是一次哈希計算, 時間複雜度爲O(1), 這極大地提升了數據的存儲性能.

 

  • 可是前面咱們還提到: 當輸入樣本量足夠大時, 不相同的輸入是會產生相同輸出的, 也就是造成哈希衝突. 這麼一來就麻煩了, 原來咱們設定的"若是產生衝突, 就意味着兩個對象相同"的規則瞬間被打破, 產生衝突的頗有多是兩個不一樣的對象!
  • 而使人欣慰的是咱們除了hashCode()方法, 還有一張王牌: equals()方法. 也就是說當兩個不相同的對象產生哈希衝突後, 咱們能夠用equals()方法進一步判斷兩個對象是否相同. 這時equals()方法就至關重要了, 這個狀況下它必需要能斷定這兩個對象是不相同的.
  • 講到這裏就引出了Java程序設計中一些重要原則:
  • 若是兩個對象是相等的, 它們的equals()方法應該要返回true, 它們的hashCode()須要返回相同的結果.
  • 但有時候面試題不會問得這麼直接, 它會問你:兩個對象的hashCdoe()相同, 它的equals()方法必定要返回true, 對嗎?
  • 那答案確定不對. 由於咱們不能保證每一個程序設計者都會遵循編碼規則, 有可能兩個不一樣對象的hashCode()會返回相同的結果. 若是你理解上面的內容, 這個問題就很好解答: 兩個對象的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;
複製代碼

結束api

  • 講到這裏但願你們對hashCode()與equals()方法能有更深刻的理解, 明白背後的設計思想與原理.
  • 我以前有一個疑問, 可能你們看完這篇文章後也會有: equals()方法平時我會用到, 因此我知道它除了和hashCode()方法有密切聯繫外, 還有別的用途. 可是hashCode()呢, 它除了和equals()方法有密切聯繫外, 還有其餘用途嗎?
  • 通過在互聯網上一番搜尋, 我目前給出的答案是沒有. 也就是說hashCode()僅在散列表中才有用,在其它狀況下沒用.
  • 固然若是這個答案不正確, 或者你還有別的思考, 歡迎留言與我交流~
相關文章
相關標籤/搜索