1、前言
在Java語言中,==、equals()、hashCode()這三個方法都和對象的比較有關,但這三個方法各有什麼用處,也就是說爲什麼要設計三種對象的比較方法呢?
2、關於==
==設計的目的就是爲比較兩個對象是否是同一個對象。比較對象的相等不僅要比較對象內容相等,還要比較對象引用地址是否相等。
對於基本數據類型而言,比較就是判斷這兩個數值是否相等,(基本數據類型沒有方法),不存在equals()和hashCode()比較的問題,下面的討論都是針對引用數據類型展開的。
對於引用對象而言,比較兩個引用變量的引用的是否是統一個對象,即比較的是兩個引用存儲中的對象地址是不是一樣。
3、關於equals()
==比較的是兩個對象是否是同一個對象,這並不能滿足很多需求。有時候當兩個對象不==的時候,我們仍然會認爲兩者是“相等”的,比如對於String對象,當兩個對象的字符串序列是一致的,我們就認爲他們是“相等”的。對於這樣的需求,需要equals()來實現。對於有這種需求的對象的類,重寫其equals()方法便可,具體的“相等”邏輯可以根據需要自己定義。
equals()和hashCode()都是從Object類中繼承而來的,而Object中equals()默認實現的是比較兩個對象是否==,即默認的equals()和==的效果是一樣的。equals()方法在Object類中定義如下:
- public boolean equals(Object obj) {
- return (this == obj);
- }
public boolean equals(Object obj) {
return (this == obj);
}
很明顯是對兩個對象的地址值進行的比較(即比較引用是否相同)。但是我們必需清楚,當String 、Math、還有Integer、Double。。。。等這些封裝類在使用equals()方法時,已經覆蓋了object類的equals()方法。比如在String類中如下:
- public boolean equals(Object anObject) {
- if (this == anObject) {
- return true;
- }
- if (anObject instanceof String) {
- String anotherString = (String)anObject;
- int n = count;
- if (n == anotherString.count) {
- char v1[] = value;
- char v2[] = anotherString.value;
- int i = offset;
- int j = anotherString.offset;
- while (n-- != 0) {
- if (v1[i++] != v2[j++])
- return false;
- }
- return true;
- }
- }
- return false;
- }
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = count;
if (n == anotherString.count) {
char v1[] = value;
char v2[] = anotherString.value;
int i = offset;
int j = anotherString.offset;
while (n-- != 0) {
if (v1[i++] != v2[j++])
return false;
}
return true;
}
}
return false;
}
String類中的equals()方法很明顯是僅僅進行了對象內容的比較,而沒有比較對象存儲地址。同理我們可知,Double,Integer,Math等等這些類的equals()方法都是被重寫了的,從而比較的都是內容的值。
我們還應該注意,Java語言對equals()的要求如下,這些要求是必須遵循的:
• 對稱性:如果x.equals(y)返回是“true”,那麼y.equals(x)也應該返回是“true”。
• 反射性:x.equals(x)必須返回是“true”。
• 類推性:如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那麼z.equals(x)也應該返回是“true”。
• 還有一致性:如果x.equals(y)返回是“true”,只要x和y內容一直不變,不管你重複x.equals(y)多少次,返回都是“true”。
• 任何情況下,x.equals(null),永遠返回是“false”;x.equals(和x不同類型的對象)永遠返回是“false”。
以上這五點是重寫equals()方法時,必須遵守的準則,如果違反會出現意想不到的結果,請大家一定要遵守。
4、關於hashCode()
hashCode()方法返回的是一個數值,稱之爲hashCode。hashCode()方法在Object類中的定義如下:
- public native int hashCode();
public native int hashCode();
這是一個本地方法,其實現根據本地機器相關。
當然我們可以再自定義類中覆蓋hashCode()方法,比如String、Integer、Double等等這些類都是覆蓋了hashCode()方法的。例如在String類中定義的hashcode()方法如下:
- public int hashCode() {
- int h = hash;
- if (h == 0) {
- int off = offset;
- char val[] = value;
- int len = count;
- for (int i = 0; i < len; i++) {
- h = 31*h + val[off++];
- }
- hash = h;
- }
- return h;
- }
public int hashCode() {
int h = hash;
if (h == 0) {
int off = offset;
char val[] = value;
int len = count;
for (int i = 0; i < len; i++) {
h = 31*h + val[off++];
}
hash = h;
}
return h;
}
解釋一下這個程序(String的API中寫到): s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
使用 int 算法,這裏 s[i] 是字符串的第 i 個字符,n 是字符串的長度,^ 表示求冪。(空字符串的哈希碼爲 0。)
5、equals()和hashCode()千絲萬縷的關係
hashCode()方法常被設計用來提高性能,其兩者的關係在於:①若兩個對象相等(equals),那麼這兩個對象一定有相同的哈希值(hashCode);②若兩個對象的哈希值相同,但這兩個對象並不一定相等。
上述兩點說明,通過equals()方法判斷兩個對象相等,那麼這兩個對象的hashCode()方法返回的hashCode一定相等;但equals()方法不相等的兩個對象,卻不一定他們的hashCode()不相等,也就是說,equals()方法不相等的兩個對象,hashcode()有可能相等。(我的理解是由於哈希碼在生成的時候產生衝突造成的)。反之,hashCode()不相等,一定能推出equals()也不等;hashCode()相等,並不能說明equals()相等或不等。
解釋下上述關係的使用範圍,我的理解是在object、String等類中都能使用。在object類中,hashCode()方法是本地方法,返回的是對象的地址值,而object類中的equals()方法比較的也是兩個對象的地址值,如果equals()相等,說明兩個對象地址值也相等,當然hashCode()也就相等了;在String類中,equals()返回的是兩個對象內容的比較,當兩個對象內容相等時,hashCode()方法根據String類的重寫(前面關於hashCode()已經說明)代碼的分析,也可知道hashCode()返回結果也會相等。以此類推,可以知道Integer、Double等封裝類中經過重寫的equals()和hashCode()方法也同樣適合於這個原則。當然沒有經過重寫的類,在繼承了object類的equals()和hashCode()方法後,也會遵守這個原則。
6、equals()和hashCode()方法在HashSet、HashMap、Hashtable中的應用
以HashSet集合爲例具體說明:
Hashset是繼承Set接口,Set接口又實現Collection接口,這是層次關係。那麼hashset是根據什麼原理來存取對象的呢?
在hashset中不允許出現重複對象,元素的位置也是不確定的。在hashset中又是怎樣判定元素是否重複的呢?這就是問題的關鍵所在,在java的集合中,判斷兩個對象是否相等的規則是:
1) 判斷兩個對象的hashCode是否相等。如果不相等,認爲兩個對象也不相等,完畢; 如果相等,轉入2)
(這一點只是爲了提高存儲效率而要求的,其實理論上沒有也可以,但如果沒有,實際使用時效率會大大降低,所以我們這裏將其做爲必需的。後面會重點講到這個問題。)
2) 判斷兩個對象用equals運算是否相等 。如果不相等,認爲兩個對象也不相等;如果相等,認爲兩個對象相等(equals()是判斷兩個對象是否相等的關鍵)
爲什麼是兩條準則,難道用第一條不行嗎?不行,因爲前面已經說了,hashCode()相等時,equals()方法也可能不等,所以必須用第2條準則進行限制,才能保證加入的爲非重複元素。
參看程序1:
- String s1 = new String("zhaoxudong");
- String s2 = new String("zhaoxudong");
- System.out.println(s1 == s2);// false
- System.out.println(s1.equals(s2));// true
- System.out.println(s1.hashCode());
- System.out.println(s2.hashCode());// s1.hashcode()等於s2.hashcode()
- Set<String> hashset = new HashSet<String>();
- hashset.add(s1);
- hashset.add(s2);
- /* 實質上在添加s1,s2時,運用上面說到的兩點準則,
- * 可以知道hashset認爲s1和s2是相等的,是在添加重複元素,
- * 所以讓s2覆蓋了s1; */
- Iterator<String> it = hashset.iterator();
- while (it.hasNext()) {
- System.out.println(it.next());
- }
String s1 = new String("zhaoxudong");
String s2 = new String("zhaoxudong");
System.out.println(s1 == s2);// false
System.out.println(s1.equals(s2));// true
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());// s1.hashcode()等於s2.hashcode()
Set<String> hashset = new HashSet<String>();
hashset.add(s1);
hashset.add(s2);
/* 實質上在添加s1,s2時,運用上面說到的兩點準則,
* 可以知道hashset認爲s1和s2是相等的,是在添加重複元素,
* 所以讓s2覆蓋了s1; */
Iterator<String> it = hashset.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
輸出結果:
- false
- true
- -967303459
- -967303459
- zhaoxudong
false
true
-967303459
-967303459
zhaoxudong
分析:這是因爲String類重寫了equals()方法和hashCode()方法,所以根據上述兩條原則進行判定時,HashSet認爲s1和s2是兩個相等的對象,故而不會進行重複添加,s2會直接覆蓋掉s1。
參看程序2:
- public class Test {
- public static void main(String[] args) {
- HashSet<Student> hs = new HashSet<Student>();
- hs.add(new Student(1, "zhangsan"));
- hs.add(new Student(2, "lisi"));
- hs.add(new Student(3, "wangwu"));
- hs.add(new Student(1, "zhangsan"));
- Iterator<Student> it = hs.iterator();
- while (it.hasNext()) {
- System.out.println(it.next());
- }
- }
- }
- class Student {
- int num;
- String name;
- Student(int num, String name) {
- this.num = num;
- this.name = name;
- }
- public String toString() {
- return num + ":" + name;
- }
- }
public class Test {
public static void main(String[] args) {
HashSet<Student> hs = new HashSet<Student>();
hs.add(new Student(1, "zhangsan"));
hs.add(new Student(2, "lisi"));
hs.add(new Student(3, "wangwu"));
hs.add(new Student(1, "zhangsan"));
Iterator<Student> it = hs.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
}
class Student {
int num;
String name;
Student(int num, String name) {
this.num = num;
this.name = name;
}
public String toString() {
return num + ":" + name;
}
}
輸出結果:
- 2:lisi
- 1:zhangsan
- 1:zhangsan
- 3:wangwu
2:lisi
1:zhangsan
1:zhangsan
3:wangwu
問題出現了,爲什麼hashset添加了相等的元素呢,這是不是和hashset的原則違背了呢?回答是:沒有 因爲在根據hashcode()對兩次建立的new Student(1,"zhangsan")對象進行比較時,生成的是不同的哈希碼值,所以hashset把他當作不同的對象對待了,當然此時的equals()方法返回的值也不等(這個不用解釋了吧)。那麼爲什麼會生成不同的哈希碼值呢?上面我們在比較s1和s2的時候不是生成了同樣的哈希碼嗎?原因就在於我們自己寫的Student類並沒有重新自己的hashcode()和equals()方法,所以在比較時,是繼承的object類中的hashcode()方法,呵呵,各位還記得object類中的hashcode()方法比較的是什麼吧!!
它是一個本地方法,比較的是對象的地址(引用地址),使用new方法創建對象,兩次生成的當然是不同的對象了(這個大家都能理解吧。。。),造成的結果就是兩個對象的hashcode()返回的值不一樣。所以根據第一個準則,hashset會把它們當作不同的對象對待,自然也用不着第二個準則進行判定了。那麼怎麼解決這個問題呢??
答案是:在Student類中重新hashcode()和equals()方法。
參考代碼:
- class Student {
- int num;
- String name;
- Student(int num, String name) {
- this.num = num;
- this.name = name;
- }
- /*hashCode()方法重寫*/
- public int hashCode() {
- return num * name.hashCode();
- }
- /*這裏用num和name作爲條件來進行比較,
- * 若num和name都相同的對象就被視爲相等的對象*/
- public boolean equals(Object o) {
- Student s = (Student) o;
- return num == s.num && name.equals(s.name);
- }
- public String toString() {
- return num + ":" + name;
- }
- }
class Student {
int num;
String name;
Student(int num, String name) {
this.num = num;
this.name = name;
}
/*hashCode()方法重寫*/
public int hashCode() {
return num * name.hashCode();
}
/*這裏用num和name作爲條件來進行比較,
* 若num和name都相同的對象就被視爲相等的對象*/
public boolean equals(Object o) {
Student s = (Student) o;
return num == s.num && name.equals(s.name);
}
public String toString() {
return num + ":" + name;
}
}
根據重寫的方法,即便兩次調用了new Student(1,"zhangsan"),我們在獲得對象的哈希碼時,根據重寫的方法hashcode(),獲得的哈希碼肯定是一樣的(這一點應該沒有疑問吧)。 當然根據equals()方法我們也可判斷是相同的。所以在向hashset集合中添加時把它們當作重複元素看待了。所以運行修改後的程序時,我們會發現運行結果是:
- 1:zhangsan
- 3:wangwu
- 2:lisi
1:zhangsan
3:wangwu
2:lisi
通過結果可知,重複的元素問題已經消除。
關於在hibernate的pojo類中,重新equals()和hashcode()的問題:
1) 重點是equals,重寫hashCode只是技術要求(爲了提高效率)
2) 爲什麼要重寫equals呢,因爲在java的集合框架中,是通過equals來判斷兩個對象是否相等的
3) 在hibernate中,經常使用set集合來保存相關對象,而set集合是不允許重複的。我們再來談談前面提到在向hashset集合中添加元素時,怎樣判斷對象是否相同的準則,前面說了兩條,其實只要重寫equals()這一條也可以。
但當hashset中元素比較多時,或者是重寫的equals()方法比較複雜時,我們只用equals()方法進行比較判斷,效率也會非常低,所以引入了hashcode()這個方法,只是爲了提高效率,但是我覺得這是非常有必要的(所以我們在前面以兩條準則來進行hashset的元素是否重複的判斷)。