java為什麼要重寫hashCode和equals方法

業餘草發表於2018-12-07

如果不被重寫(原生)的hashCode和equals是什麼樣的?

  • 不被重寫(原生)的hashCode值是根據記憶體地址換算出來的一個值。
          
  • 不被重寫(原生)的equals方法是嚴格判斷一個物件是否相等的方法(object1 == object2)。

  為什麼需要重寫equals和hashCode方法?


      在我們的業務系統中判斷物件時有時候需要的不是一種嚴格意義上的相等,而是一種業務上的物件相等。在這種情況下,原生的equals方法就不能滿足我們的需求了。


      所以這個時候我們需要重寫equals方法,來滿足我們的業務系統上的需求。那麼為什麼在重寫equals方法的時候需要重寫hashCode方法呢?


      我們先來看一下Object.hashCode的通用約定(摘自《Effective Java》第45頁)
    在一個應用程式執行期間,如果一個物件的equals方法做比較所用到的資訊沒有被修改的話,那麼,對該物件呼叫hashCode方法多次,它必須始終如一地返回 同一個整數。在同一個應用程式的多次執行過程中,這個整數可以不同,即這個應用程式這次執行返回的整數與下一次執行返回的整數可以不一致。
    如果兩個物件根據equals(Object)方法是相等的,那麼呼叫這兩個物件中任一個物件的hashCode方法必須產生同樣的整數結果。
  如果兩個物件根據equals(Object)方法是不相等的,那麼呼叫這兩個物件中任一個物件的hashCode方法,不要求必須產生不同的整數結果。然而,程式設計師應該意識到這樣的事實,對於不相等的物件產生截然不同的整數結果,有可能提高雜湊表(hash table)的效能。
     如果只重寫了equals方法而沒有重寫hashCode方法的話,則會違反約定的第二條:相等的物件必須具有相等的雜湊碼(hashCode)
     同時對於HashSet和HashMap這些基於雜湊值(hash)實現的類。HashMap的底層處理機制是以陣列的方法儲存放入的資料的(Node<K,V>[] table),其中的關鍵是陣列下標的處理。陣列的下標是根據傳入的元素hashCode方法的返回值再和特定的值異或決定的。如果該陣列位置上已經有放入的值了,且傳入的鍵值相等則不處理,若不相等則覆蓋原來的值,如果陣列位置沒有條目,則插入,並加入到相應的連結串列中。檢查鍵是否存在也是根據hashCode值來確定的。所以如果不重寫hashCode的話,可能導致HashSet、HashMap不能正常的運作、
  如果我們將某個自定義物件存到HashMap或者HashSet及其類似實現類中的時候,如果該物件的屬性參與了hashCode的計算,那麼就不能修改該物件引數hashCode計算的屬性了。有可能會移除不了元素,導致記憶體洩漏。

  擴充套件:
      在重寫equals方法的時候,需要遵守下面的通用約定:
           1、自反性。
               對於任意的引用值x,x.equals(x)一定為true。
           2、對稱性。
               對於任意的引用值x和y,當且僅當y.equals(x)返回true時,x.equals(y)也一定返回true。
           3、傳遞性。
               對於任意的引用值x、y和z,如果x.equals(y)返回true,並且y.equals(z)也返回true,那麼x.equals(z)也一定返回true。
           4、一致性。
               對於任意的引用值x和y,如果用於equals比較的物件沒有被修改的話,那麼,對此呼叫x.equals(y)要麼一致地返回true,要麼一致的返回false。
           5、對於任意的非空引用值x,x.equals(null)一定返回false。
     重寫hashCode方法的大致方式:
            a、把某個非零常數值,比如說17(最好是素數),儲存在一個叫result的int型別的變數中。
            b、對於物件中每一個關鍵域f(值equals方法中考慮的每一個域),完成一些步驟:
                1、為該域計算int型別的雜湊嗎c:
                    1)、如果該域是boolean型別,則計算(f?0:1)。
                    2)、如果該域是byte、char、short或者int型別,則計算(int)f。
                    3)、如果該域是float型別,則計算Float.floatToIntBits(f)。
                    4)、如果該域是long型別,則計算(int)(f ^ (f>>>32))。
                    5)、如果該域是double型別,則計算Double.doubleToLongBits(f)得到一個long型別的值,然後按照步驟4,對該long型值計算雜湊值。
                    6)、如果該域是一個物件引用,並且該類的equals方法通過遞迴呼叫equals的方式來比較這個域,則同樣對這個域遞迴呼叫hashCode。如果要求一個更為複雜的比較,則為這個域計算一個“規範表示”,然後針對這個正規化表示呼叫hashCode。如果這個域的值為null,則返回0(或者其他某個常數)
                    7)、如果該域是一個陣列,則把每一個元素當做單獨的域來處理。也就是說,遞迴地應用上述規則,對每個重要的元素計算一個雜湊碼,然後根據步驟下面的做法把這些雜湊值組合起來。
                2、按照下面的公式,把步驟1中計算得到的雜湊碼C組合到result中:
                    result = 31*result+c。
            c、返回result。
            d、寫完hashCode方法之後,問自己“是否相等的例項具有相等的雜湊碼”。如果不是的話,找出原因,並修改。
            可以通過org.apache.commons.lang.builder.HashCodeBuilder這個工具類來方便的重寫hashCode方法。

我的部落格即將同步至騰訊雲+社群,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=3e86itz1l0u8s

相關文章