hashcode() 方法詳解
hashCode()
方法給對象返回一個(gè)hash code值。這個(gè)方法被用于hash tables,例如HashMap。
它的性質(zhì)是:
在一個(gè)Java應(yīng)用的執(zhí)行期間,如果一個(gè)對象提供給equals做比較的信息沒有被修改的話,該對象多次調(diào)用
hashCode()
方法,該方法必須始終如一返回同一個(gè)integer。如果兩個(gè)對象根據(jù)
equals(Object)
方法是相等的,那么調(diào)用二者各自的hashCode()
方法必須產(chǎn)生同一個(gè)integer結(jié)果。并不要求根據(jù)
equals(java.lang.Object)
方法不相等的兩個(gè)對象,調(diào)用二者各自的hashCode()
方法必須產(chǎn)生不同的integer結(jié)果。然而,程序員應(yīng)該意識到對于不同的對象產(chǎn)生不同的integer結(jié)果,有可能會提高h(yuǎn)ash table的性能。
大量的實(shí)踐表明,由Object
類定義的hashCode()
方法對于不同的對象返回不同的integer。
在object類中,hashCode定義如下:
public native int hashCode();
說明是一個(gè)本地方法,它的實(shí)現(xiàn)是根據(jù)本地機(jī)器相關(guān)的。當(dāng)然我們可以在自己寫的類中覆蓋hashcode()方法,比如String、Integer、Double等這些類都是覆蓋了hashcode()方法的。例如在String類中定義的hashcode()方法如下:
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;
}
解釋一下這個(gè)程序(String的API中寫到):s[0]31^(n-1) + s[1]31^(n-2) + … + s[n-1]
使用 int 算法,這里 s[i] 是字符串的第 i 個(gè)字符,n 是字符串的長度,^ 表示求冪(空字符串的哈希碼為 0)。
想要弄明白hashCode的作用,必須要先知道Java中的集合。
總的來說,Java中的集合(Collection)有兩類,一類是List,再有一類是Set。前者集合內(nèi)的元素是有序的,元素可以重復(fù);后者元素?zé)o序,但元素不可重復(fù)。這里就引出一個(gè)問題:要想保證元素不重復(fù),可兩個(gè)元素是否重復(fù)應(yīng)該依據(jù)什么來判斷呢?
這就是Object.equals方法了。但是,如果每增加一個(gè)元素就檢查一次,那么當(dāng)元素很多時(shí),后添加到集合中的元素比較的次數(shù)就非常多了。也就是說,如果集合中現(xiàn)在已經(jīng)有1000個(gè)元素,那么第1001個(gè)元素加入集合時(shí),它就要調(diào)用1000次equals方法。這顯然會大大降低效率。
于是,Java采用了哈希表的原理。哈希(Hash)實(shí)際上是個(gè)人名,由于他提出一哈希算法的概念,所以就以他的名字命名了。哈希算法也稱為散列算法,是將數(shù)據(jù)依特定算法直接指定到一個(gè)地址上,初學(xué)者可以簡單理解,hashCode方法實(shí)際上返回的就是對象存儲的物理地址(實(shí)際可能并不是)。
這樣一來,當(dāng)集合要添加新的元素時(shí),先調(diào)用這個(gè)元素的hashCode方法,就一下子能定位到它應(yīng)該放置的物理位置上。如果這個(gè)位置上沒有元素,它就可以直接存儲在這個(gè)位置上,不用再進(jìn)行任何比較了;如果這個(gè)位置上已經(jīng)有元素了,就調(diào)用它的equals方法與新元素進(jìn)行比較,相同的話就不存了,不相同就散列其它的地址。所以這里存在一個(gè)沖突解決的問題。這樣一來實(shí)際調(diào)用equals方法的次數(shù)就大大降低了,幾乎只需要一兩次。
** 簡而言之,在集合查找時(shí),hashcode能大大降低對象比較次數(shù),提高查找效率!**
Java對象的eqauls方法和hashCode方法是這樣規(guī)定的:
1、相等(相同)的對象必須具有相等的哈希碼(或者散列碼)。
2、如果兩個(gè)對象的hashCode相同,它們并不一定相同。
關(guān)于第一點(diǎn),相等(相同)的對象必須具有相等的哈希碼(或者散列碼),為什么?
想象一下,假如兩個(gè)Java對象A和B,A和B相等(eqauls結(jié)果為true),但A和B的哈希碼不同,則A和B存入HashMap時(shí)的哈希碼計(jì)算得到的HashMap內(nèi)部數(shù)組位置索引可能不同,那么A和B很有可能允許同時(shí)存入HashMap,顯然相等/相同的元素是不允許同時(shí)存入HashMap,HashMap不允許存放重復(fù)元素。
關(guān)于第二點(diǎn),兩個(gè)對象的hashCode相同,它們并不一定相同
也就是說,不同對象的hashCode可能相同;假如兩個(gè)Java對象A和B,A和B不相等(eqauls結(jié)果為false),但A和B的哈希碼相等,將A和B都存入HashMap時(shí)會發(fā)生哈希沖突,也就是A和B存放在HashMap內(nèi)部數(shù)組的位置索引相同這時(shí)HashMap會在該位置建立一個(gè)鏈接表,將A和B串起來放在該位置,顯然,該情況不違反HashMap的使用原則,是允許的。當(dāng)然,哈希沖突越少越好,盡量采用好的哈希算法以避免哈希沖突。
所以,Java對于eqauls方法和hashCode方法是這樣規(guī)定的:
1.如果兩個(gè)對象相同,那么它們的hashCode值一定要相同;
2.如果兩個(gè)對象的hashCode相同,它們并不一定相同(這里說的對象相同指的是用eqauls方法比較)。
如不按要求去做了,會發(fā)現(xiàn)相同的對象可以出現(xiàn)在Set集合中,同時(shí),增加新元素的效率會大大下降。
3.equals()相等的兩個(gè)對象,hashcode()一定相等;equals()不相等的兩個(gè)對象,卻并不能證明他們的hashcode()不相等。
換句話說,equals()方法不相等的兩個(gè)對象,hashcode()有可能相等(我的理解是由于哈希碼在生成的時(shí)候產(chǎn)生沖突造成的)。反過來,hashcode()不等,一定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等。
在object類中,hashcode()方法是本地方法,返回的是對象的地址值,而object類中的equals()方法比較的也是兩個(gè)對象的地址值,如果equals()相等,說明兩個(gè)對象地址值也相等,當(dāng)然hashcode()也就相等了;**在String類中,equals()返回的是兩個(gè)對象內(nèi)容的比較**,當(dāng)兩個(gè)對象內(nèi)容相等時(shí),Hashcode()方法根據(jù)String類的重寫代碼的分析,也可知道hashcode()返回結(jié)果也會相等。以此類推,可以知道Integer、Double等封裝類中經(jīng)過重寫的equals()和hashcode()方法也同樣適合于這個(gè)原則。當(dāng)然沒有經(jīng)過重寫的類,在繼承了object類的equals()和hashcode()方法后,也會遵守這個(gè)原則。
Hashset、Hashmap、Hashtable與hashcode()和equals()的密切關(guān)系
Hashset是繼承Set接口,Set接口又實(shí)現(xiàn)Collection接口,這是層次關(guān)系。那么Hashset、Hashmap、Hashtable中的存儲操作是根據(jù)什么原理來存取對象的呢?
下面以HashSet為例進(jìn)行分析,我們都知道:在hashset中不允許出現(xiàn)重復(fù)對象,元素的位置也是不確定的。在hashset中又是怎樣判定元素是否重復(fù)的呢?在java的集合中,判斷兩個(gè)對象是否相等的規(guī)則是:
1.判斷兩個(gè)對象的hashCode是否相等
如果不相等,認(rèn)為兩個(gè)對象也不相等,完畢
如果相等,轉(zhuǎn)入2
(這一點(diǎn)只是為了提高存儲效率而要求的,其實(shí)理論上沒有也可以,但如果沒有,實(shí)際使用時(shí)效率會大大降低,所以我們這里將其做為必需的。)
2.判斷兩個(gè)對象用equals運(yùn)算是否相等
如果不相等,認(rèn)為兩個(gè)對象也不相等
如果相等,認(rèn)為兩個(gè)對象相等(equals()是判斷兩個(gè)對象是否相等的關(guān)鍵)
為什么是兩條準(zhǔn)則,難道用第一條不行嗎?不行,因?yàn)榍懊嬉呀?jīng)說了,hashcode()相等時(shí),equals()方法也可能不等,所以必須用第2條準(zhǔn)則進(jìn)行限制,才能保證加入的為非重復(fù)元素。
例1:
[](javascript:void(0); "復(fù)制代碼")
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;"> 1 package com.bijian.study; 2
3 import java.util.HashSet; 4 import java.util.Iterator; 5 import java.util.Set; 6
7 public class HashSetTest { 8
9 public static void main(String args[]) { 10 String s1 = new String("aaa"); 11 String s2 = new String("aaa"); 12 System.out.println(s1 == s2); 13 System.out.println(s1.equals(s2)); 14 System.out.println(s1.hashCode()); 15 System.out.println(s2.hashCode()); 16 Set hashset = new HashSet(); 17 hashset.add(s1); 18 hashset.add(s2); 19 Iterator it = hashset.iterator(); 20 while (it.hasNext()) { 21 System.out.println(it.next()); 22 } 23 } 24 }</pre>
](javascript:void(0); "復(fù)制代碼")
運(yùn)行結(jié)果:
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">false
true
96321
96321 aaa</pre>
這是因?yàn)镾tring類已經(jīng)重寫了equals()方法和hashcode()方法,所以hashset認(rèn)為它們是相等的對象,進(jìn)行了重復(fù)添加。
例2:
[](javascript:void(0); "復(fù)制代碼")
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;"> 1 package com.bijian.study; 2
3 import java.util.HashSet; 4 import java.util.Iterator; 5
6 public class HashSetTest { 7
8 public static void main(String[] args) { 9 HashSet hs = new HashSet(); 10 hs.add(new Student(1, "zhangsan")); 11 hs.add(new Student(2, "lisi")); 12 hs.add(new Student(3, "wangwu")); 13 hs.add(new Student(1, "zhangsan")); 14
15 Iterator it = hs.iterator(); 16 while (it.hasNext()) { 17 System.out.println(it.next()); 18 } 19 } 20 } 21
22 class Student { 23 int num; 24 String name; 25
26 Student(int num, String name) { 27 this.num = num; 28 this.name = name; 29 } 30
31 public String toString() { 32 return num + ":" + name; 33 } 34 }</pre>
](javascript:void(0); "復(fù)制代碼")
運(yùn)行結(jié)果:
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">1:zhangsan 3:wangwu 2:lisi 1:zhangsan </pre>
為什么hashset添加了相等的元素呢,這是不是和hashset的原則違背了呢?回答是:沒有。因?yàn)樵诟鶕?jù)hashcode()對兩次建立的new Student(1,“zhangsan”)對象進(jìn)行比較時(shí),生成的是不同的哈希碼值,所以hashset把他當(dāng)作不同的對象對待了,當(dāng)然此時(shí)的equals()方法返回的值也不等。
為什么會生成不同的哈希碼值呢?上面我們在比較s1和s2的時(shí)候不是生成了同樣的哈希碼嗎?原因就在于我們自己寫的Student類并沒有重新自己的hashcode()和equals()方法,所以在比較時(shí),是繼承的object類中的hashcode()方法,而object類中的hashcode()方法是一個(gè)本地方法,比較的是對象的地址(引用地址),使用new方法創(chuàng)建對象,兩次生成的當(dāng)然是不同的對象了,造成的結(jié)果就是兩個(gè)對象的hashcode()返回的值不一樣,所以Hashset會把它們當(dāng)作不同的對象對待。
怎么解決這個(gè)問題呢?答案是:在Student類中重新hashcode()和equals()方法。
[](javascript:void(0); "復(fù)制代碼")
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">class Student { int num;
String name;
Student(int num, String name) { this.num = num; this.name = name;
} public int hashCode() { return num * name.hashCode();
} public boolean equals(Object o) {
Student s = (Student) o; return num == s.num && name.equals(s.name);
} public String toString() { return num + ":" + name;
}
}</pre>
[](javascript:void(0); "復(fù)制代碼")
運(yùn)行結(jié)果:
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">1:zhangsan 3:wangwu 2:lisi </pre>
可以看到重復(fù)元素的問題已經(jīng)消除,根據(jù)重寫的方法,即便兩次調(diào)用了new Student(1,"zhangsan"),我們在獲得對象的哈希碼時(shí),根據(jù)重寫的方法hashcode(),獲得的哈希碼肯定是一樣的,當(dāng)然根據(jù)equals()方法我們也可判斷是相同的,所以在向hashset集合中添加時(shí)把它們當(dāng)作重復(fù)元素看待了。
重寫equals()和hashcode()小結(jié):
1.重點(diǎn)是equals,重寫hashCode只是技術(shù)要求(為了提高效率)
2.為什么要重寫equals呢?因?yàn)樵趈ava的集合框架中,是通過equals來判斷兩個(gè)對象是否相等的
3.在hibernate中,經(jīng)常使用set集合來保存相關(guān)對象,而set集合是不允許重復(fù)的。在向HashSet集合中添加元素時(shí),其實(shí)只要重寫equals()這一條也可以。但當(dāng)hashset中元素比較多時(shí),或者是重寫的equals()方法比較復(fù)雜時(shí),我們只用equals()方法進(jìn)行比較判斷,效率也會非常低,所以引入了hashCode()這個(gè)方法,只是為了提高效率,且這是非常有必要的。比如可以這樣寫:
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">public int hashCode(){ return 1; //等價(jià)于hashcode無效
} </pre>
這樣做的效果就是在比較哈希碼的時(shí)候不能進(jìn)行判斷,因?yàn)槊總€(gè)對象返回的哈希碼都是1,每次都必須要經(jīng)過比較equals()方法后才能進(jìn)行判斷是否重復(fù),這當(dāng)然會引起效率的大大降低。