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