困擾我很久的問題,一直不明白為什麼重寫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的這種散列的資料類型的運用。