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

