爲什麼重寫equals()的同時還得重寫hashCode()

 

這個問題之前我也很好奇,不過最後還是在書上得到了比較明朗的解釋,當然這個問題主要是針對映射相關的操作(Map接口)。學過數據結構的同學都知道Map接口的類會使用到鍵對象的哈希碼,當我們調用put方法或者get方法對Map容器進行操作時,都是根據鍵對象的哈希碼來計算存儲位置的,因此如果我們對哈希碼的獲取沒有相關保證,就可能會得不到預期的結果。在java中,我們可以使用hashCode()來獲取對象的哈希碼,其值就是對象的存儲地址,這個方法在Object類中聲明,因此所有的子類都含有該方法。那我們先來認識一下hashCode()這個方法吧。hashCode的意思就是散列碼,也就是哈希碼,是由對象導出的一個整型值,散列碼是沒有規律的,如果x與y是兩個不同的對象,那麼x.hashCode()與y.hashCode()基本是不會相同的,注意是“基本”,還是有很小的機率會相同,因此兩個對象hashCode一樣並不能完全說明兩個對象相等,下面通過String類的hashCode()計算一組散列碼:

​
package com.way.test;
public class HashCodeTest {
    public static void main(String[] args) {
        int hash=0;
        String s="ok";
        StringBuilder sb =new StringBuilder(s);
        
        System.out.println(s.hashCode()+"  "+sb.hashCode());
        
        String t = new String("ok");
        StringBuilder tb =new StringBuilder(s);
        System.out.println(t.hashCode()+"  "+tb.hashCode());
    }
    
}

​


運行結果:

3548  1829164700

3548  2018699554

我們可以看出,字符串s與t擁有相同的散列碼,這是因爲字符串的散列碼是由內容導出的。而字符串緩衝sb與tb卻有着不同的散列碼,這是因爲StringBuilder沒有重寫hashCode方法,它的散列碼是由Object類默認的hashCode方法計算出來的對象存儲地址,所以散列碼自然也就不同了。那麼我們該如何重寫出一個較好的hashCode方法呢,其實並不難,我們只要合理地組織對象的散列碼,就能夠讓不同的對象產生比較均勻的散列碼。例如下面的例子:

package com.way.test;
public class Model {
    private String name;
    private double salary;
    private int sex;
    
    @Override
    public int hashCode() {
        return name.hashCode()+new Double(salary).hashCode() 
                + new Integer(sex).hashCode();
    }
}


上面的代碼我們通過合理的利用各個屬性對象的散列碼進行組合,最終便能產生一個相對比較好的或者說更加均勻的散列碼,當然上面僅僅是個參考例子而已,我們也可以通過其他方式去實現,只要能使散列碼更加均勻(所謂的均勻就是每個對象產生的散列碼最好都不衝突)就行了。不過這裏有點要注意的就是java 7中對hashCode方法做了兩個改進,首先java發佈者希望我們使用更加安全的調用方式來返回散列碼,也就是使用null安全的方法Objects.hashCode(注意不是Object而是java.util.Objects)方法,這個方法的優點是如果參數爲null,就只返回0,否則返回對象參數調用的hashCode的結果。Objects.hashCode 源碼如下:

public static int hashCode(Object o) {
        return o != null ? o.hashCode() : 0;
    }


因此我們修改後的代碼如下:

package com.way.test;
import java.util.Objects;
public  class Model {
    private   String name;
    private double salary;
    private int sex;
    @Override
    public int hashCode() {
        return Objects.hashCode(name)+new Double(salary).hashCode() 
                + new Integer(sex).hashCode();
    }
}


java 7還提供了另外一個方法java.util.Objects.hash(Object... objects),當我們需要組合多個散列值時可以調用該方法。進一步簡化上述的代碼:

package com.way.test;
import java.util.Objects;
public  class Model {
    private   String name;
    private double salary;
    private int sex;
//    @Override
//    public int hashCode() {
//        return Objects.hashCode(name)+new Double(salary).hashCode() 
//                + new Integer(sex).hashCode();
//    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name,salary,sex);
    }
}


好了,到此hashCode()該介紹的我們都說了,還有一點要說的如果我們提供的是一個數值類型的變量的話,那麼我們可以調用Arrays.hashCode()來計算它的散列碼,這個散列碼是由數組元素的散列碼組成的。接下來我們迴歸到我們之前的問題,重寫equals方法時也必須重寫hashCode方法。在Java API文檔中關於hashCode方法有以下幾點規定(原文來自java深入解析一書)。

在java應用程序執行期間,如果在equals方法比較中所用的信息沒有被修改,那麼在同一個對象上多次調用hashCode方法時必須一致地返回相同的整數。如果多次執行同一個應用時,不要求該整數必須相同。

如果兩個對象通過調用equals方法是相等的,那麼這兩個對象調用hashCode方法必須返回相同的整數。

如果兩個對象通過調用equals方法是不相等的,不要求這兩個對象調用hashCode方法必須返回不同的整數。但是程序員應該意識到對不同的對象產生不同的hash值可以提供哈希表的性能。

通過前面的分析,我們知道在Object類中,hashCode方法是通過Object對象的地址計算出來的,因爲Object對象只與自身相等,所以同一個對象的地址總是相等的,計算取得的哈希碼也必然相等,對於不同的對象,由於地址不同,所獲取的哈希碼自然也不會相等。因此到這裏我們就明白了,如果一個類重寫了equals方法,但沒有重寫hashCode方法,將會直接違法了第2條規定,這樣的話,如果我們通過映射表(Map接口)操作相關對象時,就無法達到我們預期想要的效果。如果大家不相信, 可以看看下面的例子(來自java深入解析一書)

package com.way.test;
import java.util.HashMap;
import java.util.Map;
public class MapTest {
    public static void main(String[] args) {
        Map<String,Value> map1 = new HashMap<String,Value>();
        String s1 = new String("key");
        String s2 = new String("key");    
        Value value = new Value(2);
        map1.put(s1, value);
        System.out.println("s1.equals(s2):"+s1.equals(s2));
        System.out.println("map1.get(s1):"+map1.get(s1));
        System.out.println("map1.get(s2):"+map1.get(s2));
        
        
        Map<Key,Value> map2 = new HashMap<Key,Value>();
        Key k1 = new Key("A");
        Key k2 = new Key("A");
        map2.put(k1, value);
        System.out.println("k1.equals(k2):"+s1.equals(s2));
        System.out.println("map2.get(k1):"+map2.get(k1));
        System.out.println("map2.get(k2):"+map2.get(k2));
    }
    
    /**
     * 鍵
     * 
     *
     */
    static class Key{
        private String k;
        public Key(String key){
            this.k=key;
        }
        
        @Override
        public boolean equals(Object obj) {
            if(obj instanceof Key){
                Key key=(Key)obj;
                return k.equals(key.k);
            }
            return false;
        }
    }
    
    /**
     * 值
     * 
     *
     */
    static class Value{
        private int v;
        
        public Value(int v){
            this.v=v;
        }
        
        @Override
        public String toString() {
            return "類Value的值-->"+v;
        }
    }
}



代碼比較簡單,我們就不過多解釋了(注意Key類並沒有重寫hashCode方法),直接運行看結果


 

s1.equals(s2):true
map1.get(s1):類Value的值-->2
map1.get(s2):類Value的值-->2
k1.equals(k2):true
map2.get(k1):類Value的值-->2
map2.get(k2):null


對於s1和s2的結果,我們並不驚訝,因爲相同的內容的s1和s2獲取相同內的value這個很正常,因爲String類重寫了equals方法和hashCode方法,使其比較的是內容和獲取的是內容的哈希碼。但是對於k1和k2的結果就不太盡人意了,k1獲取到的值是2,k2獲取到的是null,這是爲什麼呢?想必大家已經發現了,Key只重寫了equals方法並沒有重寫hashCode方法,這樣的話,equals比較的確實是內容,而hashCode方法呢?沒重寫,那就肯定調用超類Object的hashCode方法,這樣返回的不就是地址了嗎?k1與k2屬於兩個不同的對象,返回的地址肯定不一樣,所以現在我們知道調用map2.get(k2)爲什麼返回null了吧?那麼該如何修改呢?很簡單,我們要做也重寫一下hashCode方法即可(如果參與equals方法比較的成員變量是引用類型的,則可以遞歸調用hashCode方法來實現):

@Override
public int hashCode() {
     return k.hashCode();
}


再次運行:
 

s1.equals(s2):true
map1.get(s1):類Value的值-->2
map1.get(s2):類Value的值-->2
k1.equals(k2):true
map2.get(k1):類Value的值-->2
map2.get(k2):類Value的值-->2

本文有部分內容參考了其他博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章