開發與維運

阿里雲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個對象,地址固然不同。

public static void main(String[] args) {

    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方法的結果。

public static void main(String[] args) {

    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:要是每次都返回不一樣的,就沒法玩兒了)

public static void main(String[] args) {

    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碼。

public class HashMapTest {

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一樣,後面的值會替換前面的值,接下來我們實驗下:

public class HashMapTest {

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()方法,更改後的代碼如下:

public class HashMapTest {

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的這種散列的數據類型的運用。

Leave a Reply

Your email address will not be published. Required fields are marked *