JAVA基礎-自問自答學hashCode和equals

liangzzz發表於2017-09-08

前言

hashCodeequals常常在面試中會被問到,在工作中我們也有可能遇到要重寫物件equals方法的情況,而且hashCode方法的設計思想值得我們學習,所以我們有必要去深入學習一下這兩個方法。

下面我就以面試問答的形式學習我們的——hashcodeequals方法(原始碼分析基於JDK8)

本文同步釋出於簡書:www.jianshu.com/p/ce3dbf5f0…

問答內容

1.

問:hashCode方法有了解過嗎?這個方法有什麼用?

答:從JAVA官方對hashCode方法的說明定義(定義在示例程式碼中),我們可以得知hashCode的作用有如下幾點:

  1. hashCode的存在主要用於查詢的快捷性,如HashtableHashMap等,hashCode是用來在雜湊儲存結構中確定物件的儲存地址的。

  2. 如果兩個物件相同,就是適用於equals(java.lang.Object) 方法,那麼這兩個物件的hashCode一定要相同。

  3. 如果物件的equals方法被重寫,那麼物件的hashCode也儘量重寫,並且產生hashCode使用的物件,一定要和equals方法中使用的一致,否則就會違反上面提到的第2點。

  4. 兩個物件的hashCode相同,並不一定表示兩個物件就相同,也就是不一定適用於equals(java.lang.Object) 方法,只能夠說明這兩個物件在雜湊儲存結構中,如Hashtable,他們“存放在同一個籃子裡”。

1.hashcode是用來查詢的,如果你學過資料結構就應該知道,在查詢和排序這一章有  
例如記憶體中有這樣的位置  
0  1  2  3  4  5  6  7    
而我有個類,這個類有個欄位叫ID,我要把這個類存放在以上8個位置之一,
如果不用hashcode而任意存放,那麼當查詢時就需要到這八個位置裡挨個去找,或者用二分法一類的演算法。  
但如果用hashcode那就會使效率提高很多。  
我們這個類中有個欄位叫ID,那麼我們就定義我們的hashcode為ID%8,
然後把我們的類存放在取得得餘數那個位置。比如我們的ID為9,
9除8的餘數為1,那麼我們就把該類存在1這個位置,
如果ID是13,求得的餘數是5,那麼我們就把該類放在5這個位置。
這樣,以後在查詢該類時就可以通過ID除 8求餘數直接找到存放的位置了。  

2.但是如果兩個類有相同的hashcode怎麼辦那(我們假設上面的類的ID不是唯一的),
例如9除以8和17除以8的餘數都是1,那麼這是不是合法的,
回答是:可以這樣。那麼如何判斷呢?在這個時候就需要定義 equals了。  
也就是說,我們先通過 hashcode來判斷兩個類是否存放某個桶裡,
但這個桶裡可能有很多類,那麼我們就需要再通過 equals 來在這個桶裡找到我們要的類。  
那麼。重寫了equals(),為什麼還要重寫hashCode()呢?  
想想,你要在一個桶裡找東西,你必須先要找到這個桶啊,
你不通過重寫hashcode()來找到桶,光重寫equals()有什麼用啊複製程式碼

上述回答轉載於:Java中hashCode的作用 由於作者總結的太好,所以直接轉載了

示例程式碼:

package java.lang;

public class Object {
·······

    /**
     * 返回該物件的雜湊碼值。
     * 支援此方法是為了提高雜湊表(例如 java.util.Hashtable 提供的雜湊表)的效能
     * {@link java.util.HashMap}.
     * <p>
     * hashCode 的常規協定是:
     * <ul>
     * <li>在 Java 應用程式執行期間,在對同一物件多次呼叫 hashCode 方法時,
     *     必須一致地返回相同的整數,前提是將物件進行 equals 比較時所用的資訊沒有被修改。
     *     從某一應用程式的一次執行到同一應用程式的另一次執行,該整數無需保持一致。
     * <li>如果根據 equals(Object) 方法,兩個物件是相等的,
     *     那麼對這兩個物件中的每個物件呼叫 hashCode 方法都必須生成相同的整數結果。
     * <li>如果根據 equals(java.lang.Object) 方法,兩個物件不相等,
     *     那麼對這兩個物件中的任一物件上呼叫 hashCode 方法不 要求一定生成不同的整數結果。
     *     但是,程式設計師應該意識到,為不相等的物件生成不同整數結果可以提高雜湊表的效能。
     * </ul>
     * <p>
     * 實際上,由 Object 類定義的 hashCode 方法確實會針對不同的物件返回不同的整數。
     * (這一般是通過將該物件的內部地址轉換成一個整數來實現的,
     * 但是 JavaTM 程式語言不需要這種實現技巧。)
     *
     * @return  此物件的一個雜湊碼值。
     * @see     java.lang.Object#equals(java.lang.Object)
     * @see     java.lang.System#identityHashCode
     */
    public native int hashCode();
·······
}複製程式碼

2.

問:談談你對equals(Object obj)方法的理解,它和 == 操作符相比,有什麼區別?

答:
A.== 操作符分為兩種情況:

  • 比較基礎型別(byte,short,int,long,float,double,char,boolean)時,比較的是值是否相等

  • 比較物件,比較的是物件在記憶體中的空間地址是否相等。

B.equals(Object obj)方法比較也分為兩種情況:

  • 如果一個類沒有重寫equals(Object obj)方法,則等價於通過==比較兩個物件,即比較的是物件在記憶體中的空間地址是否相等。

  • 如果重寫了equals(Object obj)方法,則根據重寫的方法內容去比較相等,返回true則相等,false則不相等。

3.

問:那如果要您去重寫equals(Object obj)方法,您會怎麼做?重寫的過程需要注意什麼?

答:我們在重寫equals(Object obj)方法,需要遵守JAVA官方的通用約定(詳細請看示例程式碼),約定簡述:

  • 自反性:對於非 null 的物件 x,必須有 x.equals(x)=true;
  • 對稱性:如果 x.equals(y)=true,那麼 y.equals(x) 必須也為true;
  • 傳遞性:如果 x.equals(y)=true 而且 y.equals(z)=true,那麼x.equals(z) 必須為true;
  • 對於非 null 的物件 x,一定有x.equals(null)=false

  • equals(Object obj)方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定宣告相等物件必須具有相等的雜湊碼。

根據上述約定,我們可以按如下步驟重寫equals(Object obj)

1). 先使用 == 操作符判斷兩個物件的引用地址是否相同。
2). 使用instanceof來判斷 兩個物件的型別是否一致。
3). 如果型別相同,則把待比較引數轉型,逐一比較兩個物件內部的值是否一致,全部一致才返回true,否則返回false
4). 重寫hashCode方法,確保相等的兩個物件必須具有相等的雜湊碼。

  • 我們在重寫一個類的hashCode方法時,最好是將所有用於相等性檢查的欄位都進行hashCode計算,最後將所有hashCode值相加,得出最終的hashCode,這樣可以保證hashCode生成均勻,不容易產生碰撞。

常見資料型別hashcode計算方式如下(參考自JDK原始碼):

重要欄位var的型別 hash運算
byte,short,int,char (int)var
long (int)(var ^ (var >>> 32))
float Float.floatToIntBits(var)
double long bits = Double.doubleToLongBits(var);分量 = (int)(bits ^ (bits >>> 32));
引用型別 (null == var ? 0 : var.hashCode())

hashCode計算-圖片來自於《Effective Java》
hashCode計算-圖片來自於《Effective Java》

示例程式碼:


    /**
     * 指示其他某個物件是否與此物件“相等”。
     * <p>
     * equals 方法在非空物件引用上實現相等關係:
     * <ul>
     * <li>自反性:對於任何非空引用值 x,x.equals(x) 都應返回 true。
     *
     * <li>對稱性:對於任何非空引用值 x 和 y,當且僅當 y.equals(x) 返回 true 時,
     * x.equals(y) 才應返回 true。
     *
     * <li>傳遞性:對於任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,
     * 並且 y.equals(z) 返回 true,那麼 x.equals(z) 應返回 true。
     *
     * <li>一致性:對於任何非空引用值 x 和 y,多次呼叫 x.equals(y) 始終返回 
     *  true 或始終返回 false,前提是物件上 equals 比較中所用的資訊沒有被修改。
     * 
     * <li>對於任何非空引用值 x,x.equals(null) 都應返回 false。
     * </ul>
     * 
     * <p>
     * Object 類的 equals 方法實現物件上差別可能性最大的相等關係;
     * 即,對於任何非空引用值 x 和 y,當且僅當 x 和 y 引用同一個物件時,
     * 此方法才返回 true(x == y 具有值 true)。
     * 
     * <p>
     * 注意:當此方法被重寫時,通常有必要重寫 hashCode 方法,
     * 以維護 hashCode 方法的常規協定,該協定宣告相等物件必須具有相等的雜湊碼。
     *
     * @param   要與之比較的引用物件。
     * @return  如果此物件與 obj 引數相同,則返回 true;否則返回 false。
     * @see     #hashCode()
     * @see     java.util.HashMap
     */
    public boolean equals(Object obj) {
        return (this == obj);
    }複製程式碼

4.

問:如果需要您去維護一個類的hash雜湊表,如何設計,如何解決hash衝突?

答:我們在設計類的hash雜湊表時,不能保證每個元素的hash值都是不一樣的,這樣就會造成hash衝突。解決hash衝突有如下4種方法:

  • 開發定址法:既然當前位置容不下衝突的元素了,那就再找一個空的位置儲存 Hash 衝突的值(當前 index 衝突了,那麼將衝突的元素放在 index+1)。

  • 再雜湊法:換一個 Hash 演算法再計算一個 hash 值,如果不衝突了就儲存值(例如第一個演算法是名字的首字母的 Hash 值,如果衝突了,計算名字的第二個字母的 Hash 值,如果衝突解決了則將值放入陣列中)。

  • 鏈地址法:每個陣列中都存有一個單連結串列,發生 Hash 衝突時,只是將衝突的 value 當作新節點插入到連結串列(HashMap 解決衝突的辦法)。

  • 公共溢位區法:將衝突的 value 都存到另外一個順序表中,查詢時如果當前表沒有對應值,則去溢位區進行順序查詢。

總結

  1. 當你真要的需要重寫equals方法,這兩點一定要記住:
  • A.如果兩個物件相等(equals() 返回 true),那麼它們的 hashCode()一定要相同;

  • B.如果兩個物件hashCode()相等,它們並不一定相等(equals() 不一定返回 true)。

  1. 如果重寫的equals方法但不重寫hashCode,都是耍流氓,會有意想不到的結果。

  2. 重寫hashCode方法時,儘可能將所有用於相等比較的引數都參與hashCode的計算。

  3. 建立hash雜湊表的意義就是在於,提高查詢效率,當資料量大時,尤為顯著。

參考文章:
Java中hashCode的作用
如何正確實現 Java 中的 HashCode
Java 的 equals 與 hashcode 對比分析
程式設計師必須搞清的概念equals和=和hashcode的區別
Android 面試準備之「equals 和 == 」

相關文章