前言
這篇文章主要幫你理解equal()
和hashcode()
這兩個方法,而后你可以將它們應(yīng)用到實際工程中;
在使用Java中collections時,添加collections中的某個元素的類的時候,應(yīng)該適當(dāng)覆蓋equal()
和hashcode()
,否則會出錯;
Object類(java中所有類的父類)定義了兩個方法equals()
和hashCode()
,這意味著java中所有的類(包括我們自己創(chuàng)建的類)都將繼承這些方法.
但是,對于將對象添加到collections中的類,必須重寫這兩方法,尤其是基于哈希表的集合,例如:HashSet和HashMap;
equals()方法的理解
當(dāng)比較兩個對象的時候,java就調(diào)用equals()方法,如果兩個對象相等,則返回true,否則返回false;(注意:equal()與==
是有很大區(qū)別的)
區(qū)別在于:
equals()
比較的是兩個對象的數(shù)據(jù)成員,而==
比較的是兩個對象的引用(即內(nèi)存地址)
注意:object類中的equals()比較的是兩個對象的引用,我們自己在寫類的時候必須將這個方法覆蓋以進(jìn)行語義比較;
JDK中幾乎所有的類都有自己的equals()方法,比如String拜轨,Date,Integer,Double等;
看下面的一個例子:
String s1 = new String("This is a string");
String s2 = new String("This is a string");
boolean refEqual = (s1 == s2);
boolean secEqual = (s1.equals(s2));
System.out.println("s1 == s2: " + refEqual);
System.out.println("s1.equals(s2): " + secEqual);
你能猜到結(jié)果么,請看:
s1 == s2: false
s1.equals(s2): true
你可以看到,引用比較(==
操作符)返回false;因為s1和s2是存儲在內(nèi)存中的不同地址;
而語義比較返回true,因為這兩個字符串的值相同(同一個字符串),故可以認(rèn)定其語義是相等 的;
再看一個例子:
public class Student {
private String id;
private String name;
private String email;
private int age;
public Student(String id, String name, String email, int age) {
this.id = id;
this.name = name;
this.email = email;
this.age = age;
}
public String toString() {
String studentInfo = "Student " + id;
studentInfo += ": " + name;
studentInfo += " - " + email;
studentInfo += " - " + age;
return studentInfo;
}
}
如果兩個Student對象具有相同的屬性(id既绕、姓名掉瞳、電子郵件和年齡),則可以認(rèn)為它們在語義上是相等的∷В現(xiàn)在秕豫,讓我們看看如何在這個類中重寫equals()方法來確認(rèn)兩個具有相同屬性的學(xué)生對象是相等的;
public boolean equals(Object obj) {
if (obj instanceof Student) {
Student another = (Student) obj;
if (this.id.equals(another.id) &&
this.name.equals(another.name) &&
this.email.equals(another.email) &&
this.age == another.age) {
return true;
}
}
return false;
}
在這里,此equals()方法檢查傳遞的對象是否為Student類型观蓄,以及它是否具有與當(dāng)前對象相同的屬性混移,相同則(返回true);否則,返回false侮穿。 讓我們用以下代碼進(jìn)行測試:
Student student1 = new Student("123", "Tom", "tom@gmail.com", 30);
Student student2 = new Student("123", "Tom", "tom@gmail.com", 30);
Student student3 = new Student("456", "Peter", "peter@gmail.com", 23);
System.out.println("student1 == student2: " + (student1 == student2));
System.out.println("student1.equals(student2): " + (student1.equals(student2)));
System.out.println("student2.equals(student3): " + (student2.equals(student3)));
測試結(jié)果:
student1 == student2: false
student1.equals(student2): true
student2.equals(student3): false
讓我們來看另一個示例,以理解重寫equals()方法的作用歌径。假設(shè)我們有一個這樣的學(xué)生列表
List<Student> listStudents = new ArrayList<>();
此列表包含上面的三個Student對象:
listStudents.add(student1);
listStudents.add(student2);
listStudents.add(student3);
現(xiàn)在,我們要檢查列表中是否包含具有給定ID的學(xué)生亲茅。 我會告訴您一個使用equals()方法的簡單有趣的解決方案回铛。
請注意,List接口提供了contains(Object)方法克锣,該方法可用于檢查列表中是否存在指定的對象茵肃。 在后臺,列表調(diào)用搜索對象上的equals()方法將其與集合中的其他對象進(jìn)行比較。
如果兩個Student對象有相同的ID袭祟,是否可以認(rèn)為它們是相等的?所以我們像這樣更新Student類中的equals()方法;
public boolean equals(Object obj) {
if (obj instanceof Student) {
Student another = (Student) obj;
if (this.id.equals(another.id)) {
return true;
}
}
return false;
}
在這里,此equals()方法僅比較兩個Student對象的ID屬性验残。 并將另一個構(gòu)造函數(shù)添加至Student類:
public Student(String id) {
this.id = id;
}
現(xiàn)在,我們可以執(zhí)行如下檢查:
Student searchStudent1 = new Student("123");
Student searchStudent4 = new Student("789");
boolean found1 = listStudents.contains(searchStudent1);
boolean found4 = listStudents.contains(searchStudent4);
System.out.println("Found student1: " + found1);
System.out.println("Found student4: " + found4);
結(jié)果如下:
Found student1: true
Found student4: false
多虧了equals()方法巾乳,使我們的代碼變得簡單.想象一下,如果不使用它您没,我們將實現(xiàn)更復(fù)雜的搜索功能,如下所示:
public boolean searchStudent(List<Student> listStudents, String id) {
for (Student student : listStudents) {
if (student.getId().equals(id)) {
return true;
}
}
return false;
}
HashCode的理解
Object類定義hashCode()方法胆绊,如下所示:
public int hashCode()
您可以看到這個方法返回一個整數(shù).那么它在哪里呢?
基于哈希表的集合(如Hashtable,HashSet和HashMap)使用此哈希數(shù)將對象存儲在稱為“存儲桶”的小容器中. 每個存儲桶都與一個哈希碼相關(guān)聯(lián)氨鹏,每個存儲桶僅包含具有相同哈希碼的對象。
換句話說压状,哈希表通過其哈希碼值將其元素分組仆抵。 這種安排通過搜索集合的一小部分而不是整個集合來幫助哈希表快速有效地定位元素。
以下是在哈希表中定位元素的步驟:
獲取指定元素的哈希碼值何缓。 這里hashCode()方法將被調(diào)用肢础。
找到與該哈希碼關(guān)聯(lián)的正確存儲桶。
在存儲桶內(nèi)部,通過將指定元素與存儲桶中的所有元素進(jìn)行比較,找到正確的元素.這將導(dǎo)致調(diào)用指定元素的equals()方法碌廓。
當(dāng)我們將類的對象添加到基于哈希表的集合(HashSet,HashMap)時,該類的hashCode()方法將被調(diào)用以產(chǎn)生一個整數(shù)(可以是任意值),集合使用此數(shù)字來快速有效地存儲和定位對象,因為基于哈希表的集合不會保持其元素的順序传轰。
注意:Object類中hashCode()的默認(rèn)實現(xiàn)返回一個整數(shù),該整數(shù)是對象的內(nèi)存地址.我們應(yīng)該在自己的類中重寫它. JDK中的幾乎所有類都會覆蓋其自己的hashCode()方法版本,例如String,Date,Integer,Double等。
hashcode()和equals()之間的規(guī)則與約定
如上所述谷婆,基于哈希表的集合由于是通過調(diào)用元素的hashCode()和equals()方法來定位元素慨蛙,因此對于重寫這些方法的方式辽聊,我們必須遵守此約定:
當(dāng)equals()方法被覆蓋時,hashCode()方法也必須被覆蓋期贫。
如果兩個對象相等,則它們的哈希碼也必須相等跟匆。
如果兩個對象不相等,則它們的哈希碼沒有限制(它們的哈希碼可以相等或不相等).
如果兩個對象具有相同的哈希碼,則它們的相等性不受限制(它們可以相等或不相等).
如果兩個對象具有不同的哈希碼,則它們不能相等。
通過遵循這些規(guī)則通砍,我們使集合在維護(hù)其元素方面保持一致玛臂。 如果違反這些規(guī)則,則集合將表現(xiàn)出異常情況封孙,例如找不到對象迹冤,或者返回錯誤的對象而不是正確的對象.
hashcode()和equal()聯(lián)合實戰(zhàn)
現(xiàn)在,讓我們回到上面那個student例子,看看hashCode()和equals()方法如何影響Set的行為的
public class Student {
private String id;
private String name;
private String email;
private int age;
public Student(String id) {
this.id = id;
}
public Student(String id, String name, String email, int age) {
this.id = id;
this.name = name;
this.email = email;
this.age = age;
}
public String toString() {
String studentInfo = "Student " + id;
studentInfo += ": " + name;
studentInfo += " - " + email;
studentInfo += " - " + age;
return studentInfo;
}
public boolean equals(Object obj) {
if (obj instanceof Student) {
Student another = (Student) obj;
if (this.id.equals(another.id)) {
return true;
}
}
return false;
}
}
注意,到目前為止,只有equals()方法被覆蓋。
我們將三個Student對象添加到HashSet中,如以下代碼所示:
Student student1 = new Student("123", "Tom", "tom@gmail.com", 30);
Student student2 = new Student("123", "Tom", "tom@gmail.com", 30);
Student student3 = new Student("456", "Peter", "peter@gmail.com", 23);
Set<Student> setStudents = new HashSet<Student>();
setStudents.add(student1);
setStudents.add(student2);
setStudents.add(student3);
現(xiàn)在虎忌,讓我們使用Lambda表達(dá)式打印此集合中所有學(xué)生的信息:
setStudents.forEach(student -> System.out.println(student));
得到結(jié)果如下:
Student 456: Peter - peter@gmail.com - 23
Student 123: Tom - tom@gmail.com - 30
Student 123: Tom - tom@gmail.com - 30
不知您是否注意到似乎有2個重復(fù)的學(xué)生(ID:123)
我們希望該集合包含不重復(fù)的元素,這為什么是可能的呢?(上面明明重復(fù)了啊)
原因在這里:
該集合對要添加的每個對象調(diào)用equals()和hashCode()方法,以確保沒有重復(fù).在我們的例子中,Student類僅覆蓋equals()方法,
從Object類繼承的hashCode()方法返回每個對象的內(nèi)存地址,該地址與equals()方法不一致(這違反了上面的約定,如下圖所示,這兩個
對象所分配的地址就不一樣).因此,該集合將student1和student2對象視為兩個不同的元素泡徙。
現(xiàn)在,讓我們重寫Student類中的hashCode()方法膜蠢,以遵守equals()和hashCode()的約定. 需要添加以下代碼:
public int hashCode() {
return 31 + id.hashCode();
}
此方法基于id屬性的哈希碼返回整數(shù)(其hashCode()方法被String類覆蓋).運行代碼以再次打印集并觀察結(jié)果:
Student 123: Tom - tom@gmail.com - 30
Student 456: Peter - peter@gmail.com - 23
調(diào)試驗證
此時student1和student2的hashcode是相等的,故這里會調(diào)用equals()函數(shù)來判斷兩個對象是是否相等
hashcode方法():通過查看源碼,我們這里可以知道:
其實hashcod的本質(zhì)就是通過某種算法將傳進(jìn)來的對象映射為一個隨機(jī)數(shù)(如果兩個對象的屬性是一樣的,其哈希碼肯定一樣!!!)
<pre style="margin: 8px 0px; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198); font-family: Menlo; font-size: 0.8rem;">public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value; for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h; }</pre>
在正確地覆蓋了equals()和hashCode()方法之后堪藐,我們還可以像這樣對集合執(zhí)行搜索:
Student searchStudent = new Student("456");
boolean found = setStudents.contains(searchStudent);
System.out.println("Found student: " + found);
結(jié)果:
Found student: true
總結(jié)
請記住以下幾點:
equals()方法比較兩個對象在語義上是否相等,例如 比較類的數(shù)據(jù)成員挑围。
hashCode()方法返回一個整數(shù)值礁竞,該整數(shù)值用于在基于哈希表的集合的存儲桶中分配元素氛琢。
并記住equals()和hashCode()方法之間的約定:
當(dāng)equals()方法被覆蓋時,hashCode()方法也必須被覆蓋。
如果兩個對象相等,則它們的哈希碼也必須相等杀迹。
如果兩個對象不相等,則它們的哈希碼沒有限制(它們的哈希碼可以相等或不相等)刃泌。
如果兩個對象具有相同的哈希碼,則它們的相等性不受限制(它們可以相等或不相等)。
如果兩個對象具有不同的哈希碼,則它們不能相等洽议。
本文正在參加“寫編程博客瓜分千元現(xiàn)金”活動,關(guān)注公眾號“饑人谷”回復(fù)“編程博客”參與活動