為什么在重寫 equals方法的同時(shí)必須重寫 hashcode方法
我們都知道Java語言是完全面向?qū)ο蟮?,在java中,所有的對(duì)象都是繼承于Object類。
其 equals 方法比較的是兩個(gè)對(duì)象的引用指向的地址,hashcode 是一個(gè)本地方法,返回的是對(duì)象地址值。Ojbect類中有兩個(gè)方法equals、hashCode,這兩個(gè)方法都是用來比較兩個(gè)對(duì)象是否相等的。
為何重寫 equals方法的同時(shí)必須重寫 hashcode方法呢
可以這樣理解:重寫了 equals 方法,判斷對(duì)象相等的業(yè)務(wù)邏輯就變了,類的設(shè)計(jì)者不希望通過比較內(nèi)存地址來比較兩個(gè)對(duì)象是否相等,而 hashcode 方法繼續(xù)按照地址去比較也沒有什么意義了,索性就跟著一起變吧。
還有一個(gè)原因來源于集合。下面慢慢說~
舉個(gè)例子:
在學(xué)校中,是通過學(xué)號(hào)來判斷是不是這個(gè)人的。
下面代碼中情景為學(xué)籍錄入,學(xué)號(hào) 123 被指定給學(xué)生 Tom,學(xué)號(hào) 456 被指定給學(xué)生 Jerry,學(xué)號(hào) 123 被失誤指定給 Lily。而在錄入學(xué)籍的過程中是不應(yīng)該出現(xiàn)學(xué)號(hào)一樣的情況的。
根據(jù)情景需求是不能添加重復(fù)的對(duì)象,可以通過 HashSet 實(shí)現(xiàn)。
public class Test {
public static void main(String[] args) {
Student stu = new Student(123,"Tom");
HashSet<Student> set = new HashSet<>();
set.add(stu);
set.add(new Student(456, "Jerry"));
set.add(new Student(123, "Lily"));
Iterator<Student> iterator = set.iterator();
while (iterator.hasNext()) {
Student student = iterator.next();
System.out.println(student.getStuNum() + " --- " + student.getName());
}
}
};
class Student {
private int stuNum;
private String name;
public Student(int stuNum,String name){
this.stuNum = stuNum;
this.name = name;
}
public int getStuNum() {
return stuNum;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object obj) {
if(this==obj)
return true;
if(obj instanceof Student){
if(this.getStuNum()==((Student)obj).getStuNum())
return true;
}
return false;
}
}
輸出為:
123 --- Lily
456 --- Jerry
123 --- Tom
根據(jù)輸出我們發(fā)現(xiàn),再次將學(xué)號(hào) 123 指定給 Lily 居然成功了。到底哪里出了問題呢?
我們看一下 HashSet 的 add 方法:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
其實(shí) HashSet 是通過 HashMap 實(shí)現(xiàn)的,由此我們追蹤到 HashMap 的 put 方法:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
1.根據(jù) key,也就是 HashSet 所要添加的對(duì)象,得到 hashcode,由 hashcode 做特定位運(yùn)算得到 hash 碼;
2.利用 hash 碼定位找到數(shù)組下標(biāo),得到鏈表的鏈?zhǔn)祝?/p>
3.遍歷鏈表尋找有沒有相同的 key,判斷依據(jù)是 e.hash == hash && ((k = e.key) == key || key.equals(k))。在add Lily 的時(shí)候,由于重寫了 equals 方法,遍歷到 Tom 的時(shí)候第二個(gè)條件應(yīng)該是 true;但是因?yàn)?hashcode 方法還是使用父類的,故而 Tom 和 Lily的 hashcode 不同也就是 hash 碼不同,第一個(gè)條件為 false。這里得到兩個(gè)對(duì)象是不同的所以 HashSet 添加 Lily 成功。
總結(jié)出來原因是沒有重寫 hashcode 方法,下面改造一下:
public class Test {
public static void main(String[] args) {
Student stu = new Student(123,"Tom");
HashSet<Student> set = new HashSet<>();
set.add(stu);
set.add(new Student(456, "Jerry"));
set.add(new Student(123, "Lily"));
Iterator<Student> iterator = set.iterator();
while (iterator.hasNext()) {
Student student = iterator.next();
System.out.println(student.getStuNum() + " --- " + student.getName());
}
}
};
class Student {
private int stuNum;
private String name;
public Student(int stuNum,String name){
this.stuNum = stuNum;
this.name = name;
}
public int getStuNum() {
return stuNum;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object obj) {
if(this==obj)
return true;
if(obj instanceof Student){
if(this.getStuNum()==((Student)obj).getStuNum())
return true;
}
return false;
}
@Override
public int hashCode() {
return getStuNum();
}
}
輸出:
456 --- Jerry
123 --- Tom
重寫了 hashcode 方法返回學(xué)號(hào)。OK,大功告成。
有人可能會(huì)奇怪,e.hash == hash && ((k = e.key) == key || key.equals(k)) 這個(gè)條件是不是有點(diǎn)復(fù)雜了,我感覺只使用 equals 方法就可以了啊,為什么要多此一舉去判斷 hashcode 呢?
因?yàn)樵?HashMap 的鏈表結(jié)構(gòu)中遍歷判斷的時(shí)候,特定情況下重寫的 equals 方法比較對(duì)象是否相等的業(yè)務(wù)邏輯比較復(fù)雜,循環(huán)下來更是影響查找效率。所以這里把 hashcode 的判斷放在前面,只要 hashcode 不相等就玩兒完,不用再去調(diào)用復(fù)雜的 equals 了。很多程度地提升 HashMap 的使用效率。
所以重寫 hashcode 方法是為了讓我們能夠正常使用 HashMap 等集合類,因?yàn)?HashMap 判斷對(duì)象是否相等既要比較 hashcode 又要使用 equals 比較。而這樣的實(shí)現(xiàn)是為了提高 HashMap 的效率。
相關(guān)文章
Mybatis-Plus的saveOrUpdateBatch(null)問題及解決
這篇文章主要介紹了Mybatis-Plus的saveOrUpdateBatch(null)問題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
Spring Boot Event Bus用法小結(jié)
Spring Boot Event Bus是Spring框架中事件驅(qū)動(dòng)編程的一部分,本文主要介紹了Spring Boot Event Bus用法小結(jié),感興趣的可以了解一下2023-09-09
詳解Spring中@Autowired注解是如何實(shí)現(xiàn)的
在使用java?config的過程當(dāng)中,我們不可避免地會(huì)有各種各樣的注解打交道,其中,我們使用最多的注解應(yīng)該就是@Autowired注解了,這篇文章就來和大家聊聊它到底怎么實(shí)現(xiàn)的吧2023-07-07
SpringBoot實(shí)現(xiàn)PDF添加水印的示例
本文主要介紹了SpringBoot實(shí)現(xiàn)PDF添加水印的示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05
java JDBC系列教程之JDBC類的簡(jiǎn)析與JDBC的基礎(chǔ)操作
這篇文章主要介紹了java JDBC系列教程之JDBC類的簡(jiǎn)析與JDBC的基礎(chǔ)操作,本文分步驟通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07
這一次搞懂Spring自定義標(biāo)簽以及注解解析原理說明
這篇文章主要介紹了這一次搞懂Spring自定義標(biāo)簽以及注解解析原理說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-08-08
Java數(shù)據(jù)結(jié)構(gòu)之棧的線性結(jié)構(gòu)詳解
從數(shù)據(jù)結(jié)構(gòu)上看棧和隊(duì)列都是線性表,不過是兩種特殊的線性表,棧只允許在的一端進(jìn)行插人或刪除操作,而隊(duì)列只允許在表的一端進(jìn)行插人操作、而在另一端進(jìn)行刪除操作,這篇文章主要給大家介紹了關(guān)于Java數(shù)據(jù)結(jié)構(gòu)之棧的線性結(jié)構(gòu)的相關(guān)資料,需要的朋友可以參考下2021-08-08

