天天看點

阿裡雲java開發手冊為什麼規定隻要重寫 equals,就必須重寫 hashCode?

困擾我很久的問題,一直不明白為什麼重寫equals()方法的時候要重寫hashCode()方法,這次總算弄明白了,作此分享,如有不對之處,望大家指正。

一、equals()方法

先說說equals()方法。

  檢視Java的Object.equals()方法,如下:

public boolean equals(Object object){

return(this == obj);           

}

可以看到這裡直接用'=='來直接比較,引用《Java程式設計思想》裡的一句話:“關系操作符生成的是一個boolean結果,它們計算的是操作數的值之間的關系”。那麼'=='比較的值到底是什麼呢?

  我們知道Java有8種基本類型:數值型(byte、short、int、long、float、double)、字元型(char)、布爾型(boolean),對于這8種基本類型的比較,變量存儲的就是值,是以比較的就是'值'本身。如下,值相等就是true,不等就是false。

public static void main(String[] args) {

int a=3;                                           
    int b=4;
    int c=3;
    System.out.println(a==b);   //false
    System.out.println(a==c);   //true
}           

對于非基本類型,也就是常說的引用資料類型:類、接口、數組,由于變量種存儲的是記憶體中的位址,并不是'值'本身,是以真正比較的是該變量存儲的位址,可想而知,如果聲明的時候是2個對象,位址固然不同。

String str1 = new String("123");
    String str2 = new String("123");
    System.out.println(str1 == str2);  //false
}           

可以看到,上面這種比較方法,和Object類中的equals()方法的具體實作相同,之是以為false,是因為直接比較的是str1和str2指向的位址,也就是說Object中的equals方法是直接比較的位址,因為Object類是所有類的基類,是以調用新建立的類的equals方法,比較的就是兩個對象的位址。那麼就有人要問了,如果就是想要比較引用類型實際的值是否相等,該如何比較呢?

    铛铛铛...... 重點來了

要解決上面的問題,就是今天要說的equals(),具體的比較由各自去重寫,比較具體的值的大小。我們可以看看上面字元串的比較,如果調用String的equals方法的結果。

String str1 = new String("123");
    String str2 = new String("123");
    System.out.println(str1.equals(str2));  //true
}           

可以看到傳回的true,由興趣的同學可以去看String equals()的源碼。

是以可以通過重寫equals()方法來判斷對象的值是否相等,但是有一個要求:equals()方法實作了等價關系,即:

自反性:對于任何非空引用x,x.equals(x)應該傳回true;

對稱性:對于任何引用x和y,如果x.equals(y)傳回true,那麼y.equals(x)也應該傳回true;

傳遞性:對于任何引用x、y和z,如果x.equals(y)傳回true,y.equals(z)傳回true,那麼x.equals(z)也應該傳回true;

一緻性:如果x和y引用的對象沒有發生變化,那麼反複調用x.equals(y)應該傳回同樣的結果;

非空性:對于任意非空引用x,x.equals(null)應該傳回false;

二、hashCode()方法

此方法傳回對象的哈希碼值,什麼是哈希碼?度娘找到的相關定義:

哈希碼産生的依據:哈希碼并不是完全唯一的,它是一種算法,讓同一個類的對象按照自己不同的特征盡量的有不同的哈希碼,但不表示不同的對象哈希碼完全不同。也有相同的情況,看程式員如何寫哈希碼的算法。

簡單了解就是一套算法算出來的一個值,且這個值對于這個對象相對唯一。雜湊演算法有一個協定:在 Java 應用程式執行期間,在對同一對象多次調用 hashCode 方法時,必須一緻地傳回相同的整數,前提是将對象進行hashcode比較時所用的資訊沒有被修改。(ps:要是每次都傳回不一樣的,就沒法玩兒了)

List<Long> test1 = new ArrayList<Long>();
    test1.add(1L);
    test1.add(2L);
    System.out.println(test1.hashCode());  //994
    test1.set(0,2L);
    System.out.println(test1.hashCode());  //1025
}           

三、标題解答

首先來看一段代碼:

public class HashMapTest {

private int a;

public HashMapTest(int a) {
    this.a = a;
}

public static void main(String[] args) {
    Map<HashMapTest, Integer> map = new HashMap<HashMapTest, Integer>();
    HashMapTest instance = new HashMapTest(1);
    map.put(instance, 1);
    Integer value = map.get(new HashMapTest(1));
    if (value != null) {
        System.out.println(value);
    } else {
        System.out.println("value is null");
    }
} 
           

//程式運作結果: value is null

簡單說下HashMap的原理,HashMap存儲資料的時候,是取的key值的哈希值,然後計算數組下标,采用鍊位址法解決沖突,然後進行存儲;取資料的時候,依然是先要擷取到hash值,找到數組下标,然後for周遊連結清單集合,進行比較是否有對應的key。比較關心的有2點:1.不管是put還是get的時候,都需要得到key的哈希值,去定位key的數組下标; 2.在get的時候,需要調用equals方法比較是否有相等的key存儲過。

  反過來,我們再分析上面那段代碼,Map的key是我們自己定義的一個類,可以看到,我們沒有重寫equal方法,更沒重寫hashCode方法,意思是map在進行存儲的時候是調用的Object類中equals()和hashCode()方法。為了證明,我們列印下hashCode碼。

private Integer a;

public HashMapTest(int a) {
    this.a = a;
}

public static void main(String[] args) {
    Map<HashMapTest, Integer> map = new HashMap<HashMapTest, Integer>();
    HashMapTest instance = new HashMapTest(1);
    System.out.println("instance.hashcode:" + instance.hashCode());
    map.put(instance, 1);
    HashMapTest newInstance = new HashMapTest(1);
    System.out.println("newInstance.hashcode:" + newInstance.hashCode());
    Integer value = map.get(newInstance);
    if (value != null) {
        System.out.println(value);
    } else {
        System.out.println("value is null");
    }
}           

//運作結果:

//instance.hashcode:929338653

//newInstance.hashcode:1259475182

//value is null

不出所料,hashCode不一緻,是以對于為什麼拿不到資料就很清楚了。這2個key,在Map計算的時候,可能數組下标就不一緻,就算資料下标碰巧一緻,根據前面,最後equals比較的時候也不可能相等(很顯然,這是2個對象,在堆上的位址必定不一樣)。我們繼續往下看,假如我們重寫了equals方法,将這2個對象都put進去,根據map的原理,隻要是key一樣,後面的值會替換前面的值,接下來我們實驗下:

private Integer a;

public HashMapTest(int a) {
    this.a = a;
}

public static void main(String[] args) {
    Map<HashMapTest, Integer> map = new HashMap<HashMapTest, Integer>();
    HashMapTest instance = new HashMapTest(1);
    HashMapTest newInstance = new HashMapTest(1);
    map.put(instance, 1);
    map.put(newInstance, 2);
    Integer value = map.get(instance);
    System.out.println("instance value:"+value);
    Integer value1 = map.get(newInstance);
    System.out.println("newInstance value:"+value1);

}

public boolean equals(Object o) {
    if(o == this) {
        return true;
    } else if(!(o instanceof HashMapTest)) {
        return false;
    } else {
        HashMapTest other = (HashMapTest)o;
        if(!other.canEqual(this)) {
            return false;
        } else {
            Integer this$data = this.getA();
            Integer other$data = other.getA();
            if(this$data == null) {
                if(other$data != null) {
                    return false;
                }
            } else if(!this$data.equals(other$data)) {
                return false;
            }

            return true;
        }
    }
}
protected boolean canEqual(Object other) {
    return other instanceof HashMapTest;
}

public void setA(Integer a) {
    this.a = a;
}

public Integer getA() {
    return a;
}           

//instance value:1

//newInstance value:2

你會發現,不對呀?同樣的一個對象,為什麼在map中存了2份,map的key值不是不能重複的麼?沒錯,它就是存的2份,隻不過在它看來,這2個的key是不一樣的,因為他們的哈希碼就是不一樣的,可以自己測試下,上面列印的hash碼确實不一樣。那怎麼辦?隻有重寫hashCode()方法,更改後的代碼如下:

private Integer a;

public HashMapTest(int a) {
    this.a = a;
}

public static void main(String[] args) {
    Map<HashMapTest, Integer> map = new HashMap<HashMapTest, Integer>();
    HashMapTest instance = new HashMapTest(1);
    System.out.println("instance.hashcode:" + instance.hashCode());
    HashMapTest newInstance = new HashMapTest(1);
    System.out.println("newInstance.hashcode:" + newInstance.hashCode());
    map.put(instance, 1);
    map.put(newInstance, 2);
    Integer value = map.get(instance);
    System.out.println("instance value:"+value);
    Integer value1 = map.get(newInstance);
    System.out.println("newInstance value:"+value1);

}

public boolean equals(Object o) {
    if(o == this) {
        return true;
    } else if(!(o instanceof HashMapTest)) {
        return false;
    } else {
        HashMapTest other = (HashMapTest)o;
        if(!other.canEqual(this)) {
            return false;
        } else {
            Integer this$data = this.getA();
            Integer other$data = other.getA();
            if(this$data == null) {
                if(other$data != null) {
                    return false;
                }
            } else if(!this$data.equals(other$data)) {
                return false;
            }

            return true;
        }
    }
}
protected boolean canEqual(Object other) {
    return other instanceof HashMapTest;
}

public void setA(Integer a) {
    this.a = a;
}

public Integer getA() {
    return a;
}

public int hashCode() {
    boolean PRIME = true;
    byte result = 1;
    Integer $data = this.getA();
    int result1 = result * 59 + ($data == null?43:$data.hashCode());
    return result1;
}           

運作結果:

instance.hashcode:60

newInstance.hashcode:60

instance value:2

newInstance value:2

可以看到,他們的hash碼是一緻的,且最後的結果也是預期的。

完美的分界線

ps.總結:對于這個問題,是比較容易被忽視的,曾經同時趟過這坑,Map中存了2個數值一樣的key,是以大家謹記喲! 在重寫equals方法的時候,一定要重寫hashCode方法。

最後一點:有這個要求的症結在于,要考慮到類似HashMap、HashTable、HashSet的這種散列的資料類型的運用。