引言
我們知道Java中的集合(Collection)大致可以分為兩類,一類是List,再有一類是Set。
前者集合內的元素是有序的,元素可以重復;后者元素無序,但元素不可重復。
這里就引出一個問題:要想保證元素不重復應該依據什么來判斷呢?
為什么要用hashCode()?
為了解決放入重復數據的問題,一開始開發者們想到了用Object.equals方法。
但是,很快他們發現如果每增加一個元素就檢查一次,那么當元素很多時,后添加到集合中的元素比較的次數就非常多了。
也就是說,如果集合中現在已經有1000個元素,那么第1001個元素加入集合時,它就要調用1000次equals方法。這顯然會大大降低效率。
於是,Java采用了哈希表的原理。哈希(Hash)實際上是個人名,由於他提出“哈希算法”的概念,所以就以他的名字命名了。
哈希算法也稱為散列算法,哈希值也稱為散列碼,實際上就是將數據依照哈希算法直接指定到一個地址上。
初學者可以簡單理解,hashCode方法實際上返回的就是對象存儲的物理地址(實際可能並不是)。
這樣一來,當集合要添加新的元素時,先調用這個元素的hashCode方法,就一下子能定位到它應該放置的物理位置上。
如果這個位置上沒有元素,它就可以直接存儲在這個位置上,不用再進行任何比較了;
如果這個位置上已經有元素了,就調用它的equals方法與新元素進行比較,相同的話就不存了,不相同就散列其它的地址。
所以這里存在一個沖突解決的問題。這樣一來實際調用equals方法的次數就大大降低了,幾乎只需要一兩次。
兩個方法的作用
equals()作用:用於判斷其他對象是否與該對象相同;
Object類中是這樣定義equals():
public boolean equals(Object obj) { return (this == obj); }
很顯然,在Object類原生代碼中比較的是引用地址,但是需要提醒的一點是在String、Math、Integer、Double等封裝類中都對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; }
顯然,在String類中的equals()比較的不再是引用對象的地址而是內容,在Java8種基本數據類型中equals()比較的都是內容,其實就是數值。
HashCode()作用:給不同對象返回不同的hash code值,相當於識別碼;
使用HashCode()時應當符合以下三點:
- 在一個Java應用的執行期間,如果一個對象提供給equals做比較的信息沒有被修改的話,該對象無論調用多少次hashCode(),必須始終返回同一個integer;
- 如果兩個對象根據equals(Object)方法是相等的,那么調用二者各自的hashCode()必須產生同一個integer結果;
- 調用equals(java.lang.Object)方法結果不相等的兩個對象,調用二者各自hashCode()不一定不相同,可能相同,可能不同。
重點
在集合查找時,使用hashcode無疑能大大降低對象比較次數,提高查找效率!
Java對象的eqauls方法和hashCode方法是這樣規定的:
1、相等(相同)的對象必須具有相等的哈希碼(或者散列碼)。
2、如果兩個對象的hashCode相同,它們並不一定相同。
可能的困惑
一、相等(相同)的對象必須具有相等的哈希碼(或者散列碼),為什么?
假設A與B這兩個對象相等,即他們equals的結果為true,但他們各自的哈希碼不相同,但他們要存入同一個HashMap時,有可能就會因為哈希碼不同導致計算得出的HashMap內部數組位置索引不一樣,那么A、B很可能同時存入同一個HashMap中,但我們知道HashMap是不允許存放重復元素的。
二、為什么兩個對象的hashCode相同他們也不一定相同?
你的問題其實也是在說不同對象的hashCode有可能相同,產生這種結果的原因我個人覺得是由於“哈希算法”在生產哈希碼時造成的,兩個對象在某些方面具有高度一致性。正因為考慮到可能會出現這樣的情況,所以HashMap在添加兩個hashCode完全相同的對象時會在此哈希碼指定的內部數組的位置索引處建立一個新的鏈表,然后將兩個對象串起來放在該位置,這樣就能在保證雖然hashCode相同仍能存入HashMap中,當然前提是他們調用equals()的返回值為false。
再補充一點,在業界中有一個專門的術語去描述這種現象,我們稱之為哈希沖突,很顯然,雖然哈希沖突是可以解決的,但沒有人會希望經常看到它。
實際操作
在實際編寫代碼程序時,我們經常會被要求重寫hashCode()和equals(),曾經我也對這個問題百思不得其解,但現在我也能向大家解釋這其中的秘密了。
以HashSet為例,我們知道HashSet是繼承Set接口,而Set接口由實現了Collection接口,HashSet中不允許出現重復值,而且元素的位置也是不確定的。
那么在這里介紹一下Java集合判斷兩個對象是否相等的規則是:
1.首先要判斷兩個對象的hashCode是否相等;
如果相等,進入第二步再判斷;
如果不相等,那么認為兩個對象也不相等,結束判斷。
2.判斷兩個對象用equals()是否相等。
如果這次判斷也相等,則認為兩個對象相等;
如果不相等,那么認為兩個對象也不相等。
為什么要進行兩次判斷呢?
可以不進行第一次的判斷,但如果沒有,實際使用效率會大大降低,尤其是在進行大量數據比較時。其實前面在介紹hashCode()時有過提及,即hashCode()相等時,equals()也可能不相等,所以我們就加上了第二條判斷進行限制。總的來說,就是可以沒有第一條判斷,但必須要有第二條判斷,但在實際開發中兩條都最好寫上,一旦出現大量數據需要判斷時,僅靠equals()進行判斷的話執行效率會大打折扣。
代碼展示
package Exercise;
import java.util.HashSet; public class e1 { public static void main(String[] args) { HashSet hs=new HashSet(); hs.add(new Student(1,"張三")); hs.add(new Student(2,"李四")); hs.add(new Student(3,"王麻子")); hs.add(new Student(1,"張三")); for (Object object : hs) { System.out.println(object); } } } class Student{ int num; String name; Student(int num,String name){ this.name=name; this.num=num; } public String toString(){ return num+":"+name; } }
運行結果:
為什么Hashset添加了相等的元素呢,這是不是和Hashset的原則違背了呢?回答是:沒有。因為在根據hashCode()對兩次建立的new Student(1,“張三 ”)對象進行比較時,生成的是不同的哈希碼值,所以Hashset把他當作不同的對象對待了,當然此時的equals()方法返回的值也不等。
為什么會生成不同的哈希碼值呢?原因就在於我們自己寫的Student類並沒有重新自己的hashCode()和equals()方法,所以在比較時,是繼承的object類中的hashCode(),而object類中的hashCode()是一個本地方法,比較的是對象的地址(引用地址),使用new方法創建對象,兩次生成的當然是不同的對象了,造成的結果就是兩個對象的hashCode()返回的值不一樣,所以Hashset會把它們當作不同的對象對待。
怎么解決這個問題呢?答案是:在Student類中重寫hashCode()和equals()方法。
package Exercise; import java.util.HashSet; public class e1 { public static void main(String[] args) { HashSet hs=new HashSet(); hs.add(new Student(1,"張三")); hs.add(new Student(2,"李四")); hs.add(new Student(3,"王麻子")); hs.add(new Student(1,"張三")); for (Object object : hs) { System.out.println(object); } } } class Student{ int num; String name; Student(int num,String name){ this.name=name; this.num=num; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode()); result = prime * result + num; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Student other = (Student) obj; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; if (num != other.num) return false; return true; } public String toString(){ return num+":"+name; } }
運行結果
可以看到重復元素的問題已經消除,根據重寫的方法,即便兩次調用了new Student(1,"張三"),我們在獲得對象的哈希碼時,根據重寫的方法hashCode(),獲得的哈希碼肯定是一樣的,當然根據equals()方法我們也可判斷是相同的,所以在向hashset集合中添加時把它們當作重復元素看待了。
剛才使用的重寫是用快捷鍵進行的,我們也可以手敲,不過寫這么多就沒必要了。
手敲重寫代碼:
public int hashCode(){ return num * name.hashCode(); } public boolean equals(Object o){ Student s = (Student) o; return num == s.num && name.equals(s.name); }
做個總結
- 重點是equals,重寫hashCode只是技術要求(為了提高效率);
- 為什么要重寫equals呢?因為在Java的集合框架中,是通過equals來判斷兩個對象是否相等的;
- 在hibernate中,經常使用set集合來保存相關對象,而set集合是不允許重復的。在向HashSet集合中添加元素時,其實只要重寫equals()這一條也可以。但當hashset中元素比較多時,或者是重寫的equals()方法比較復雜時,我們只用equals()方法進行比較判斷,效率也會非常低,所以引入了hashCode()這個方法,只是為了提高效率,且這是非常有必要的。
如果hashCode()這樣寫:
public int hashCode(){ return 1; //等價於hashcode無效 }
這樣做的效果就是在比較哈希碼的時候不能進行判斷,因為每個對象返回的哈希碼都是1,每次都必須要經過比較equals()方法后才能進行判斷是否重復,這當然會引起效率的大大降低。
文章參考: