困擾我很久的問題,一直不明白為什么重寫equals()方法的時(shí)候要重寫hashCode()方法飒筑,這次總算弄明白了绽昏,作此分享全谤,如有不對(duì)之處认然,望大家指正卷员。
一、equals()方法
先說說equals()方法削饵。
??查看Java的Object.equals()方法,如下:
public boolean equals(Object object){
return(this == obj);
}
可以看到這里直接用'=='來(lái)直接比較未巫,引用《Java編程思想》里的一句話:“關(guān)系操作符生成的是一個(gè)boolean結(jié)果窿撬,它們計(jì)算的是操作數(shù)的值之間的關(guān)系”。那么'=='比較的值到底是什么呢叙凡?
??我們知道Java有8種基本類型:數(shù)值型(byte劈伴、short、int握爷、long跛璧、float、double)新啼、字符型(char)、布爾型(boolean)师抄,對(duì)于這8種基本類型的比較漓柑,變量存儲(chǔ)的就是值,所以比較的就是'值'本身叨吮。如下辆布,值相等就是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
}
對(duì)于非基本類型锋玲,也就是常說的引用數(shù)據(jù)類型:類、接口涵叮、數(shù)組惭蹂,由于變量種存儲(chǔ)的是內(nèi)存中的地址,并不是'值'本身割粮,所以真正比較的是該變量存儲(chǔ)的地址盾碗,可想而知,如果聲明的時(shí)候是2個(gè)對(duì)象舀瓢,地址固然不同廷雅。
public static void main(String[] args) {
String str1 = new String("123");
String str2 = new String("123");
System.out.println(str1 == str2); //false
}
可以看到,上面這種比較方法,和Object類中的equals()方法的具體實(shí)現(xiàn)相同航缀,之所以為false商架,是因?yàn)橹苯颖容^的是str1和str2指向的地址,也就是說Object中的equals方法是直接比較的地址芥玉,因?yàn)镺bject類是所有類的基類蛇摸,所以調(diào)用新創(chuàng)建的類的equals方法,比較的就是兩個(gè)對(duì)象的地址灿巧。那么就有人要問了赶袄,如果就是想要比較引用類型實(shí)際的值是否相等,該如何比較呢砸烦?
????鐺鐺鐺...... 重點(diǎn)來(lái)了
要解決上面的問題弃鸦,就是今天要說的equals(),具體的比較由各自去重寫幢痘,比較具體的值的大小唬格。我們可以看看上面字符串的比較,如果調(diào)用String的equals方法的結(jié)果颜说。
public static void main(String[] args) {
String str1 = new String("123");
String str2 = new String("123");
System.out.println(str1.equals(str2)); //true
}
可以看到返回的true购岗,由興趣的同學(xué)可以去看String equals()的源碼。
所以可以通過重寫equals()方法來(lái)判斷對(duì)象的值是否相等门粪,但是有一個(gè)要求:equals()方法實(shí)現(xiàn)了等價(jià)關(guān)系喊积,即:
- 自反性:對(duì)于任何非空引用x,x.equals(x)應(yīng)該返回true玄妈;
- 對(duì)稱性:對(duì)于任何引用x和y乾吻,如果x.equals(y)返回true,那么y.equals(x)也應(yīng)該返回true拟蜻;
- 傳遞性:對(duì)于任何引用x绎签、y和z,如果x.equals(y)返回true酝锅,y.equals(z)返回true诡必,那么x.equals(z)也應(yīng)該返回true;
- 一致性:如果x和y引用的對(duì)象沒有發(fā)生變化搔扁,那么反復(fù)調(diào)用x.equals(y)應(yīng)該返回同樣的結(jié)果爸舒;
- 非空性:對(duì)于任意非空引用x,x.equals(null)應(yīng)該返回false稿蹲;
二扭勉、hashCode()方法
此方法返回對(duì)象的哈希碼值,什么是哈希碼场绿?度娘找到的相關(guān)定義:
哈希碼產(chǎn)生的依據(jù):哈希碼并不是完全唯一的剖效,它是一種算法嫉入,讓同一個(gè)類的對(duì)象按照自己不同的特征盡量的有不同的哈希碼焰盗,但不表示不同的對(duì)象哈希碼完全不同璧尸。也有相同的情況,看程序員如何寫哈希碼的算法熬拒。
簡(jiǎn)單理解就是一套算法算出來(lái)的一個(gè)值爷光,且這個(gè)值對(duì)于這個(gè)對(duì)象相對(duì)唯一。哈希算法有一個(gè)協(xié)定:在 Java 應(yīng)用程序執(zhí)行期間澎粟,在對(duì)同一對(duì)象多次調(diào)用 hashCode 方法時(shí)蛀序,必須一致地返回相同的整數(shù),前提是將對(duì)象進(jìn)行hashcode比較時(shí)所用的信息沒有被修改活烙。(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
}
三、標(biāo)題解答
首先來(lái)看一段代碼:
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");
}
}
}
//程序運(yùn)行結(jié)果: value is null
簡(jiǎn)單說下HashMap的原理啸盏,HashMap存儲(chǔ)數(shù)據(jù)的時(shí)候重贺,是取的key值的哈希值,然后計(jì)算數(shù)組下標(biāo)回懦,采用鏈地址法解決沖突气笙,然后進(jìn)行存儲(chǔ);取數(shù)據(jù)的時(shí)候怯晕,依然是先要獲取到hash值潜圃,找到數(shù)組下標(biāo),然后for遍歷鏈表集合舟茶,進(jìn)行比較是否有對(duì)應(yīng)的key谭期。比較關(guān)心的有2點(diǎn):1.不管是put還是get的時(shí)候,都需要得到key的哈希值吧凉,去定位key的數(shù)組下標(biāo)隧出; 2.在get的時(shí)候,需要調(diào)用equals方法比較是否有相等的key存儲(chǔ)過客燕。
??反過來(lái)鸳劳,我們?cè)俜治錾厦婺嵌未a,Map的key是我們自己定義的一個(gè)類也搓,可以看到赏廓,我們沒有重寫equal方法,更沒重寫hashCode方法傍妒,意思是map在進(jìn)行存儲(chǔ)的時(shí)候是調(diào)用的Object類中equals()和hashCode()方法幔摸。為了證實(shí),我們打印下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");
}
}
}
//運(yùn)行結(jié)果:
//instance.hashcode:929338653
//newInstance.hashcode:1259475182
//value is null
不出所料既忆,hashCode不一致,所以對(duì)于為什么拿不到數(shù)據(jù)就很清楚了。這2個(gè)key患雇,在Map計(jì)算的時(shí)候跃脊,可能數(shù)組下標(biāo)就不一致,就算數(shù)據(jù)下標(biāo)碰巧一致苛吱,根據(jù)前面酪术,最后equals比較的時(shí)候也不可能相等(很顯然,這是2個(gè)對(duì)象翠储,在堆上的地址必定不一樣)绘雁。我們繼續(xù)往下看,假如我們重寫了equals方法援所,將這2個(gè)對(duì)象都put進(jìn)去庐舟,根據(jù)map的原理,只要是key一樣住拭,后面的值會(huì)替換前面的值挪略,接下來(lái)我們實(shí)驗(yàn)下:
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;
}
}
//運(yùn)行結(jié)果:
//instance value:1
//newInstance value:2
你會(huì)發(fā)現(xiàn),不對(duì)呀废酷?同樣的一個(gè)對(duì)象瘟檩,為什么在map中存了2份,map的key值不是不能重復(fù)的么澈蟆?沒錯(cuò)墨辛,它就是存的2份,只不過在它看來(lái)趴俘,這2個(gè)的key是不一樣的睹簇,因?yàn)樗麄兊墓4a就是不一樣的,可以自己測(cè)試下寥闪,上面打印的hash碼確實(shí)不一樣太惠。那怎么辦?只有重寫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;
}
}
//運(yùn)行結(jié)果:
//instance.hashcode:60
//newInstance.hashcode:60
//instance value:2
//newInstance value:2
可以看到凿渊,他們的hash碼是一致的,且最后的結(jié)果也是預(yù)期的缚柳。
完美的分界線
ps.總結(jié):對(duì)于這個(gè)問題埃脏,是比較容易被忽視的,曾經(jīng)同時(shí)趟過這坑秋忙,Map中存了2個(gè)數(shù)值一樣的key彩掐,所以大家謹(jǐn)記喲! 在重寫equals方法的時(shí)候灰追,一定要重寫hashCode方法堵幽。
最后一點(diǎn):有這個(gè)要求的癥結(jié)在于狗超,要考慮到類似HashMap、HashTable朴下、HashSet的這種散列的數(shù)據(jù)類型的運(yùn)用努咐。