1. 程式人生 > >為什麼重寫equals後要重寫hashCode

為什麼重寫equals後要重寫hashCode

equals和hashCode的關係

要搞清楚題目中的問題就必須搞明白equals方法和hashCode方法分別是什麼,和誕生的原因,當搞明白了這一點其實題目就不算是個問題了,下面我們來探討分別探討一下兩者代表的意義。

hashCode

筆者看到很多地方都對hashCode有兩個誤解

  • 物件預設的hashCode是物件的地址。
  • 預設的equals會先比較物件的hashCode,如果hashCode相同則代表兩個物件是同一個物件。

在這裡筆者先給出這兩個問題的結論,後面會給出證明。

  1. hashCode並不是物件的地址。
  2. 預設的equals比較的是物件的地址,與hashCode無關。

事實上想求證hashCode是不是物件的地址這件事情說容易也容易,說難也難。其實筆者在網上有很多不知出處與權威性的文章都寫hashCode就是物件的地址,從這點上來說,想找到真實答案也挺不容易的,“謊言重複千遍便是真理”說的大概就是這個意思。之所以說容易是因為只要通過閱讀Oracle的JavaAPI註釋便可知道正確答案,所以其實學習一個東西最好的辦法還是看官方的文件。但因為Oracle的API是英文的,對母語不是英文的我們來說或許會有些痛苦,即時你能看懂英文文件,為了容易我們也可能選擇找中文的文章來看,不幸的是大多數軟體的文件沒有中文的。

OracleAPI中對hashCode()的註釋如下:

  1. Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by HashMap.
  2. As much as is reasonably practical, the hashCode method defined by class Object does return distinct integers for distinct objects. (This is typically implemented by converting the internal address of the object into an integer
    , but this implementation technique is not required by the JavaTM programming language.
    )

第2句話的意思是說,不同的Object物件的返回不同的hashCode,這通常通過將物件地址進行某種轉換對映為一個integer,但並不限制具體的實現方法。換句話說,hashCode的生成策略是由jdk的實現決定的。這已經能夠說明hashCode並不等於物件的實體地址,雖然實現方式與其有關,但絕不意味著相等。其實通過下面的程式碼我們也可以從某種程度上推測證明兩者並不嚴格相等。

public class B {
    public static void main(String[] args) {
        B b1 = new B();
        B b2 = new B();
        System.out.println(b1.hashCode());
        System.out.println(b2.hashCode());
    }
}
> 356573597
> 1735600054

這個程式碼非常簡單,從一開始啟動虛擬機器到b1和b2的記憶體分配之間並沒有任何其他的過多幹擾,換句話說,堆記憶體的空閒是很多的,並不存在記憶體分配中的指標碰撞或者需要維護不連續的記憶體空閒列表,因此b1和b2的記憶體分配是相當連續的。如果hashCode代表著記憶體地址,那麼兩者應該相差不大,但事實上兩者看不出任何記憶體分佈上的聯絡。

在來解釋第一句話,這句話的意思是說一些基於hash的資料結構如HashMap等會受益於此方法,這就可以做出推測,hashCode的出現是為一些基於hash的資料結構服務的。後面我們會分析HashMap是如何根據hashCode去提升效能的,這裡必須提到JVM的一個細節:java物件在記憶體分配之後,hashCode存在於物件頭中,但這個值並不是記憶體分配完成之後就有的,當第一次呼叫物件的hashCode方法,物件的hashCode值就會存放在物件頭中。

至此,關於hashCode的第一個誤解已經解決了,下面我們證明第二個,來看下面的程式碼。

public class B {
    @Override
    public int hashCode() {
        return 1;
    }
    public static void main(String[] args) {
        B b1 = new B();
        B b2 = new B();
        System.out.println(b1.equals(b2));
        System.out.println(b1 == b2);
    }
}
> false
> false

如上所示,b1和b2擁有相同的hashCode,但是不管是equals還是==比較,都返回了false,這至少證明了Object的equals方法與hashCode並無任何關聯,檢視Object的equals方法原始碼便知。

public class Object {
    public boolean equals(Object obj) {
        return (this == obj);
    }
}

equals

equals比hashCode好理解的多,它的設計初衷是為了讓程式設計人員自己定義兩個物件是否相等,這與地址無關。因為對於java虛擬機器來講,只有兩個引用指向同一個物件,兩個物件才能看作是相等的。當然,這個原因也不是筆者憑空猜測的,OracleAPI中有這兩句話如下:

public boolean equals(Object obj)
Indicates whether some other object is “equal to” this one.
The equals method for class Object implements the most discriminating possible equivalence relation on objects; that is, for any non-null reference values x and y, this method returns true if and only if x and y refer to the same object (x == y has the value true).

但其實下面還有一句話如下:

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.

這句話告訴我們當一個物件的hashCode方法被重寫的時候,為了保持hashCode的常規協定,建議重寫hashCode方法,這裡所指的hashCode常規協定如下:

If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.

這條contract告訴我們,如果兩個物件equals,則他們要有相同的hashCode,這並不是必須滿足的條件,事實上我們很可能經常不遵守這個協定,比如下面的程式碼:

public class B {
    @Override
    public boolean equals(Object obj) {
        return true;
    }
    public static void main(String[] args) {
        B b1 = new B();
        B b2 = new B();
        System.out.println(b1.equals(b2));
        System.out.println(b1.hashCode() == b2.hashCode());
    }
}

既然這個協定不是必須要遵守的,為什麼Java建議我們如果重寫了equals方法就要重寫hashCode方法,還告訴我們如果兩個物件equals要有相同的hashCode呢?

equals & hashCode & HashSet

前文提到,Java中的hashCode主要是為了一些使用hash的資料結構而存在的。這裡以HashSet舉例,Set中是不允許物件有重複的,這裡的重複就是相等的元素,注意:這裡要分是兩個元素是實體地址上的相等,還是通過equals比較的相等。從邏輯上來說,如果兩個元素被使用者定義了的equals方法比較的結果為true,那麼不管兩個物件hashCode值是否相等,它們能應該被定義為“重複”,但事實上如果不重寫hashCode,兩個equals的物件輸出的hashCode不同,它仍然被當作不同的元素被HashSet, HashMap等一系列hash的資料結構對待。

public class B {
    @Override
    public boolean equals(Object obj) {
        return true;
    }
    public static void main(String[] args) {
        B b1 = new B();
        B b2 = new B();
        HashSet<Object> set = new HashSet();
        set.add(b1);
        set.add(b2);
        System.out.println(set.size());
    }
}
> 2

以上程式碼set中的元素為兩個,儘管b1.equals(b2) = true這在邏輯上就與元素存放不同物件相違背了(這裡以HashSet舉例,實際上任何類似的使用hash的資料結構都可以如此推導),因此Java告訴我們,如果重寫了equals方法,請務必重寫hashCode方法,使得兩個equals的物件擁有相同的hashCode,可以被hash的集合類當作相同的元素看待。

我們順便來看一下set.add(E e)方法的內容:

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

這裡呼叫了HashMap的put方法(HashSet就是用HashMap實現的),我們繼續跟進去:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 先比較hash, 在比較地址,最後呼叫equals
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            ....
        }
        if (e != null) { // existing mapping for key
            ....
        }
    }
    ...
}

需要說明的是這裡的hash並不是物件的hashCode,而是通過下面的方式處理後的結果

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

但由於相同的輸入有相同的輸出,這裡姑且把hash當作hashCode處理,當hashSet呼叫add方法時,會判斷HashMap中hash對應的bucket中是否有元素,如果有,判斷兩個元素的hash值是否相同,如果不同,HashMap會直接當作不同(邏輯上的)的元素處理,如果相同,還會比較equals和地址是否相同來判定該物件是否真的相同。

邏輯聽起來好像有點繞,簡單來說就是HashMap認為,如果兩個物件hashCode不同,那這兩個物件就不相等,如果hashCode相同,則根據地址和equals判定。

綜上所述,正式因為這些基於hash的資料結構,才使得我們在重寫equals時要重寫hashCode,否則在這些集合類中關於兩個物件是否相等的判定會在語義上變得不嚴謹,除此之外,equals和hashCode再無任何關聯。

相關推薦

為什麼重寫equals重寫hashCode

equals和hashCode的關係 要搞清楚題目中的問題就必須搞明白equals方法和hashCode方法分別是什麼,和誕生的原因,當搞明白了這一點其實題目就不算是個問題了,下面我們來探討分別探討一下兩者代表的意義。 hashCode 筆者看到很多地

重寫equals遵守的約定

一個 int 取數 整數 lse fin hash 為什麽 操作 1.自反性對於任何非null的引用的值x;x.equals(x);必須返回的是true2.對稱性對於任何非null的引用值x和y,當且僅當x.equals(y)為true的時候,y.equals(x)也必須返

java中為什麼重寫equals時必須重寫hashCode方法?

在上一篇博文Java中equals和==的區別中介紹了Object類的equals方法,並且也介紹了我們可在重寫equals方法,本章我們來說一下為什麼重寫equals方法的時候也要重寫hashCode方法。  先讓我們來看看Object類原始碼 /** * Returns a

java中為什麽重寫equals時必須重寫hashCode方法?

你在 多次調用 uci tran boolean != private 列數 codes 在上一篇博文Java中equals和==的區別中介紹了Object類的equals方法,並且也介紹了我們可在重寫equals方法,本章我們來說一下為什麽重寫equals方法的時候也要重

list\set等容器(集合)那裡重寫equals為什麼還要重寫hashCode方法

我們學些java j2se的時候為還說比較兩個引用是否值(內容)相等的時候不去重寫hashcode方法,只是重寫equals方法呢: 一下是單純重寫equals方法的例子: /**  * 測試重寫equals方法  * @author Rick  *  */ public

重寫equals方法時重寫hashcode方法的必要性

開發十年,就只剩下這套架構體系了! >>>   

為什麼繼承HttpSevlet類時不需要重寫service 而重寫doGet doPost呢?

在學習Servlet的過程中,我們大多時候編碼都是直接繼承HttpServlet這個類,並且重寫doGet ,doPost,但是檢視Api時我們會發現Servlet介面 ,GenericSevlet抽象類 以及HttpServlet類中都有service方法,那麼為什麼我們繼

為什麼重寫equals方法一定重寫hashcode方法

重寫了equals方法一定要重寫hashcode方法,原因在於用到hash來提高效率的集合類在插入物件時先比較物件的hashcode是否相同,若相同再比較equals是否相同,若hashcode不同j就不再比較equals。 雜湊表這個資料結構想必大多數人都不陌生,而且

JAVA中重寫equals()方法的同時重寫hashcode()方法

內存地址 his mov bool args 變量 維護 log obj object對象中的 public boolean equals(Object obj),對於任何非空引用值 x 和 y,當且僅當 x 和 y 引用同一個對象時,此方法才返回 true;註意:當此方法

java為什麽重寫hashCodeequals方法?

有時 不同 遞歸 步驟 原生 下標 set .com 底層 如果不被重寫(原生)的hashCode和equals是什麽樣的? 不被重寫(原生)的hashCode值是根據內存地址換算出來的一個值。 不被重寫(原生)的equals方法是嚴格判斷一個對象

【java基礎】重寫equals()方法的同時重寫hashCode()方法

而且 通過 才會 默認 什麽 需要 現在 ash 字段 1、 為什麽要重寫equals方法? 因為Object的equal方法默認是兩個對象的引用的比較,意思就是指向同一內存,地址則相等,否則不相等;如果你現在需要利用對象裏面字段的值來判斷是否相等,則重寫equals方法。

為什麼重寫equals() 和 hashcode() 方法

重寫equals() 是為了保證比如new ArrayList().contains(Object)的基於equals() 做比較的可用性 重寫hashcode() 是為了保證比如new hashMap().put(Object)的基於hashcode() 做key值的可用性 &n

為什麽重寫equalshashcode方法

.get hash -a style radi his string 了解 com equals hashcode 當新建一個java類時,需要重寫equals和hashcode方法,大家都知道!但是,為什麽要重寫呢? 需要保證對象調用equals方法為tru

java中重寫equals方法為什麼重寫hashcode方法

參考博文:https://www.cnblogs.com/dolphin0520/p/3681042.html hashcode方法作用 hashcode方法是Object類的本地方法,public native int hashcode(); Java中hashcode方法主要用於雜湊

Effective Java 第三版讀書筆記——條款11:重寫 equals 方法的同時也重寫 hashCode 方法

在每一個重寫 equals 方法的類中,都要重寫 hashCode 方法。如果不這樣做,你的類會違反 hashCode 的通用約定,這會阻止它在 HashMap 和 HashSet 這樣的集合中正常工作。下面是根據 Object 原始碼改編的約定: 在一個應用程式執行過程中,如果在 equal

為什麼重寫equalsHashCode方法

同事問我的題,說我答不上來沒法留在公司工作,答的不是很好,特此整理一下。 對於這個問題,我覺得首先應該去思考的是原來的equals方法和HashCode方法是什麼樣的。 原生的hashCode值是根據記憶體地址換算出來的一個值。 原生的equals方法是嚴格判斷

【JAVA】為什麼重寫equals(),就必須重寫hashCode()?

                  為什麼重寫equals(),就必須要重寫hashCode()? 一、equals與hashCode到底是什麼? (1)equals()方法 檢視Object的原始碼可知 public boolean equals(Object

為什麼重寫hashcode()和equals()方法

以JDK1.8原始碼詳解。 一、Object類的hashcode和equals方法 equals方法原始碼: /** * Indicates whether some other object is "equal to" this one. * <p

總結,為什麼重寫hashset的hashcode()和equals()?

看了非常多部落格,怕自己忘記了,通俗易懂的總結如下   本人總結下: 重寫前,比較地址,hashcode方法如果相等不一定是同一個物件,所以再用equals再比記憶體地址 重寫後,比較值,重寫hashCode方法後,值相同的不同物件返回的是同樣

總結,為什麽重寫hashset的hashcode()和equals()?

content 比對 不同 對象 對比 總結 text 一個 直接 看了非常多博客,怕自己忘記了,通俗易懂的總結如下 本人總結下: 重寫前,比較地址,hashcode方法如果相等不一定是同一個對象,所以再用equals再比內存地址 重寫後,比較值,重寫hashCo