2015年4月4日 星期六

hashcode方法淺析

原文: http://wenku.baidu.com/view/9070f05c312b3169a451a4af.html

有許多人學了很長時間的Java,但一直不明白hashCode方法的作用,我來解釋一下吧。

首先,想要明白hashCode的作用,你必須要先知道Java中的集合。總的來說,Java中的集合

(Collection)有兩類,一類是List,再有一類是Set。你知道它們的區別嗎?前者集合內的元

素是有序的,元素可以重複;後者元素無序,但元素不可重複。那麼這裡就有一個比較嚴重

的問題了:要想保證元素不重複,可兩個元素是否重複應該依據什麼來判斷呢?這就是

Object.equals方法了。但是,如果每增加一個元素就檢查一次,那麼當元素很多時,後添加到

集合中的元素比較的次數就非常多了。也就是說,如果集合中現在已經有1000個元素,那麼

第1001個元素加入集合時,它就要調用1000次equals方法。這顯然會大大降低效率。於是,

Java採用了哈希表的原理。

哈希(Hash)實際上是個人名,由於他提出一哈希算法的概念,所以就以他的名字命名了。

哈希算法也稱為散列算法,是將數據依特定算法直接指定到一個地址上。如果詳細講解哈希

算法,那需要更多的文章篇幅,我在這裡就不介紹了。初學者可以這樣理解,hashCode方法

實際上返回的就是對象存儲的物理地址(實際可能並不是)。

這樣一來,當集合要添加新的元素時,先調用這個元素的hashCode方法,就一下子能定位到

它應該放置的物理位置上。如果這個位置上沒有元素,它就可以直接存儲在這個位置上,不

用再進行任何比較了;如果這個位置上已經有元素了,就調用它的equals方法與新元素進行比

較,相同的話就不存了,不相同就散列其它的地址。所以這裡存在一個衝突解決的問題。這

樣一來實際調用equals方法的次數就大大降低了,幾乎只需要一兩次。所以,Java對於eqauls

方法和hashCode方法是這樣規定的:1、如果兩個對象相同,那麼它們的hashCode值一定要相

同;2、如果兩個對象的hashCode相同,它們並不一定相同上面說的對象相同指的是用eqauls

方法比較。 你當然可以不按要求去做了,但你會發現,相同的對象可以出現在Set集合中。同

時,增加新元素的效率會大大下降。


java 中hashcode() 和equals() 的詳解


1. 首先equals() 和hashcode() 這兩個方法都是從object 類中繼承過來的。


equals() 方法在object 類中定義如下:


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;


}


很明顯,這是進行的內容比較,而已經不再是地址的比較。依次類推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() 方法時,必須遵守的準則,如果違反會出現意想不到的結果,請大家一定要遵守。


2. 其次是hashcode() 方法,在object 類中定義如下:


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;


}


解釋一下這個程序(String 的API 中寫到):


s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]


使用 int 算法,這裡 s[i] 是字符串的第 i 個字符,n 是字符串的長度,^ 表示求冪。(空字符串的哈希碼為 0 。)





3. 這裡我們首先要明白一個問題:


equals() 相等的兩個對象,hashcode() 一定相等;


equals ()不相等的兩個對象,卻並不能證明他們的hashcode() 不相等。換句話說,equals()方法不相等的兩個對象,hashcode() 有可能相等。(我的理解是由於哈希碼在生成的時候產生衝突造成的)。


反過來:hashcode() 不等,一定能推出equals() 也不等;hashcode() 相等,equals() 可能相等,也可能不等。解釋下第3 點的使用範圍,我的理解是在object 、String 等類中都能使用。在object 類中,hashcode() 方法是本地方法,返回的是對象的地址值,而object 類中的equals() 方法比較的也是兩個對象的地址值,如果equals() 相等,說明兩個對象地址值也相等,當然hashcode() 也就相等了;在String 類中,equals() 返回的是兩個對象內容的比較,當兩個對象內容相等時,


Hashcode() 方法根據String 類的重寫(第2 點裡面已經分析了)代碼的分析,也可知道hashcode() 返回結果也會相等。以此類推,可以知道Integer 、Double 等封裝類中經過重寫的equals() 和hashcode() 方法也同樣適合於這個原則。當然沒有經過重寫的類,在繼承了object 類的equals() 和hashcode() 方法後,也會遵守這個原則。





4. 談到hashcode() 和equals() 就不能不說到hashset,hashmap,hashtable 中的使用,具體是怎樣呢,請看如下分析:


Hashset 是繼承Set 接口,Set 接口又實現Collection 接口,這是層次關係。那麼hashset 是根據什麼原理來存取對象的呢?


在hashset 中不允許出現重複對象,元素的位置也是不確定的。在hashset 中又是怎樣判定元素是否重複的呢?這就是問題的關鍵所在,經過一下午的查詢求證終於獲得了一點啟示,和大家分享一下,在java 的集合中,判斷兩個對像是否相等的規則是:


1) ,判斷兩個對象的hashCode 是否相等


如果不相等,認為兩個對像也不相等,完畢


如果相等,轉入2)


(這一點只是為了提高存儲效率而要求的,其實理論上沒有也可以,但如果沒有,實際使用時效率會大大降低,所以我們這裡將其做為必需的。後面會重點講到這個問題。)


2) ,判斷兩個對像用equals 運算是否相等


如果不相等,認為兩個對像也不相等


如果相等,認為兩個對象相等(equals() 是判斷兩個對像是否相等的關鍵)


為什麼是兩條準則,難道用第一條不行嗎?不行,因為前面已經說了,hashcode() 相等時,equals() 方法也可能不等,所以必須用第2 條準則進行限制,才能保證加入的為非重複元素。


比如下面的代碼:






public static void main(String args[]){


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());//s1.hashcode() 等於s2.hashcode()


System.out.println(s2.hashCode());


Set hashset=new HashSet();


hashset.add(s1);


hashset.add(s2);


/* 實質上在添加s1,s2 時,運用上面說到的兩點準則,可以知道hashset 認為s1 和s2 是相等的,是在添加重複元素,所以讓s2 覆蓋了s1;*/


Iterator it=hashset.iterator();


while(it.hasNext())


{


System.out.println(it.next());


}


最後在while 循環的時候只打印出了一個” zhaoxudong ”。


輸出結果為:false


true


-967303459


-967303459


這是因為String 類已經重寫了equals() 方法和hashcode() 方法,所以在根據上面的第1.2 條原則判定時,hashset 認為它們是相等的對象,進行了重複添加。


但是看下面的程序:


import java.util.*;


public class HashSetTest


{


public static void main(String[] args)


{


HashSet hs=new HashSet();


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 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;


}


}


輸出結果為:


1:zhangsan


1:zhangsan


3:wangwu


2:lisi


問題出現了,為什麼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;


}


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;


}


}


根據重寫的方法,即便兩次調用了new Student(1,"zhangsan") ,我們在獲得對象的哈希碼時,根據重寫的方法hashcode() ,獲得的哈希碼肯定是一樣的(這一點應該沒有疑問吧)。


當然根據equals() 方法我們也可判斷是相同的。所以在向hashset 集合中添加時把它們當作重複元素看待了。所以運行修改後的程序時,我們會發現運行結果是:


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 的元素是否重複的判斷)。


比如可以這樣寫:


public int hashCode(){


return 1;}// 等價於hashcode 無效


這樣做的效果就是在比較哈希碼的時候不能進行判斷,因為每個對象返回的哈希碼都是1 ,每次都必須要經過比較equals() 方法後才能進行判斷是否重複,這當然會引起效率的大大降低。


我有一個問題,如果像前面提到的在hashset 中判斷元素是否重複的必要方法是equals() 方法(根據網上找到的觀點),但是這裡並沒有涉及到關於哈希表的問題,可是這個集合卻叫hashset ,這是為什麼??


我想,在hashmap,hashtable 中的存儲操作,依然遵守上面的準則。所以這裡不再多說。這些是今天看書,網上查詢資料,自己總結出來的,部分代碼和語言是引述,但是千真萬確是自己總結出來的。有錯誤之處和不詳細不清楚的地方還請大家指出,我也是初學者,所以難免會有錯誤的地方,希望大家共同討論。










1. '=='是用來比較兩個變量(基本類型和對像類型)的值是否相等的,如果兩個變量是基本類型的,那很容易,直接比較值就可以了。如果兩個變量是對像類型的,那麼它還是比較值,只是它比較的是這兩個對像在棧中的引用(即地址)。
對像是放在堆中的,棧中存放的是對象的引用(地址)。由此可見'=='是對棧中的值進行比較的。如果要比較堆中對象的內容是否相同,那麼就要重寫equals方法了。
2. Object類中的equals方法就是用'=='來比較的,所以如果沒有重寫equals方法,equals和==是等價的。
通常我們會重寫equals方法,讓equals比較兩個對象的內容,而不是比較對象的引用(地址)因為往往我們覺得比較對象的內容是否相同比比較對象的引用(地址)更有意義。
3. Object類中的hashCode是返回對像在內存中地址轉換成的一個int值(可以就當做地址看)。所以如果沒有重寫hashCode方法,任何對象的hashCode都是不相等的。通常在集合類的時候需要重寫hashCode方法和equals方法,因為如果需要給集合類(比如:HashSet)添加對象,那麼在添加之前需要查看給集合裡是否已經有了該對象,比較好的方式就是用hashCode。
4.注意的是String、Integer、Boolean、Double等這些類都重寫了equals和hashCode方法,這兩個方法是根據對象的內容來比較和計算hashCode的。(詳細可以查看jdk下的String.java源代碼),所以只要對象的基本類型值相同,那麼hashcode就一定相同。
5. equals()相等的兩個對象,hashcode()一般是相等的,最好在重寫equals()方法時,重寫hashcode()方法; equals()不相等的兩個對象,卻並不能證明他們的hashcode()不相等。換句話說,equals()方法不相等的兩個對象,hashcode()有可能相等。反過來:hashcode()不等,一定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等。在object類中,hashcode()方法是本地方法,返回的是對象的引用(地址值),而object類中的equals()方法比較的也是兩個對象的引用(地址值),如果equals()相等,說明兩個對像地址值也相等,當然hashcode()也就相等了。
以下是測試代碼。


Java代碼


1. public class Equals_HashCode {


2. public static void main(String[] args) {


3. String a = new String("str");


4. String b = new String("str");


5. System.out.println(a==b);


6. System.out.println(a.equals(b));


7. System.out.println(a.hashCode());


8. System.out.println(b.hashCode());


9. //輸出false true 114225 114225


10. class A{


11. String str;


12. int i;


13. public A(String str, int i) {


14. super();


15. this.str = str;


16. this.i = i;


17. }


18. }


19. A aA = new A("str", 1 );


20. A bA = new A("str", 1 );


21. System.out.println(aA==bA);


22. System.out.println(aA.equals(bA));


23. System.out.println(aA.hashCode());


24. System.out.println(bA.hashCode());


25. //輸出false false 6413875 21174459


26. class B{


27. String str;


28. public B(String str){


29. this.str = str;


30. }


31. }


32. B aB = new B("str");


33. B bB = new B("str");


34. System.out.println(aB==bB);


35. System.out.println(aB.equals(bB));


36. System.out.println(aB.hashCode());


37. System.out.println(bB.hashCode());


38. //輸出false false 827574 17510567


39. class C{


40. int i;


41. public C(int i){


42. this.i = i;


43. }


44. }


45. C aC = ​​new C( 1 );


46. ​​C bC = new C( 1 );


47. System.out.println(aC==bC);


48. System.out.println(aC.equals(bC));


49. System.out.println(aC.hashCode());


50. System.out.println(bC.hashCode());


51. //輸出false false 27744459 28737396


52. }


53.


54. }


55.










java中equal和==的比較


java中equals方法和“==”的區別:

equals方法是java.lang.Object類的方法。

有兩種用法說明:

(1)對於字符串變量來說,使用“==”和“equals()”方法比較字符串時,其比較方法不同。

“==”比較兩個變量本身的值,即兩個對像在內存中的首地址。

“equals()”比較字符串中所包含的內容是否相同。

比如:

String s1,s2,s3 = "abc", s4 ="abc" ;

s1 = new String("abc");

s2 = new String("abc");

那麼:

s1==s2是false //兩個變量的內存地址不一樣,也就是說它們指向的對像不一樣,

故不相等。

s1.equals(s2)是true //兩個變量的所包含的內容是abc,故相等。

注意(1):

如果: StringBuffer s1 = new StringBuffer("a");

StringBuffer s2 = new StringBuffer("a");

結果: s1.equals(s2) //是false

解釋:StringBuffer類中沒有重新定義equals這個方法,因此這個方法就來自Object類,

而Object類中的equals方法是用來比較“地址”的,所以等於false.

注意(2):

對於s3和s4來說,有一點不一樣要引起注意,由於s3和s4是兩個字符

串常量所生成的變量,其中所存放的內存地址是相等的,

所以s3==s4是true(即使沒有s3=s4這樣一個賦值語句)

(2)對於非字符串變量來說,"=="和"equals"方法的作用是相同的都是用來比較其

對像在堆內存的首地址,即用來比較兩個引用變量是否指向同一個對象。

比如:

class A

{

A obj1 = new A();

A obj2 = new A();

}

那麼:obj1==obj2是false

obj1.equals(obj2)是false

但是如加上這樣一句:obj1=obj2;

那麼obj1==obj2是true

obj1.equals(obj2)是true

總之:equals方法對於字符串來說是比較內容的,而對於非字符串來說是比較

其指向的對像是否相同的。

==比較符也是比較指向的對像是否相同的也就是對像在對內存中的的首地址。

String類中重新定義了equals這個方法,而且比較的是值,而不是地址。所以是true。

關於equals與==的區別從以下幾個方面來說:

(1)如果是基本類型比較,那麼只能用==來比較,不能用equals

比如:

public class TestEquals {

public static void main(String[] args)

{

int a = 3;

int b = 4;

int c = 3;

System.out.println(a == b);//結果是false

System.out.println(a == c);//結果是true

System.out.println(a.equals(c));//錯誤,編譯不能通過,equals方法

//不能運用與基本類型的比較

}

}

(2)對於基本類型的包裝類型,比如Boolean、 Character、Byte、Shot、Integer、Long、Float、Double等的引用變量,==是比較地址的,而equals是比較內容的。比如:

public class TestEquals {

public static void main(String[] args)

{ Integer n1 = new Integer(30);

Integer n2 = new Integer(30);

Integer n3 = new Integer(31);

System.out.println( n1 == n2);//結果是false兩個不同的Integer對象,故其地址不同,

System.out.println(n1 == n3);//那麼不管是new Integer(30)還是new Integer(31 )結果都顯示false

System.out.println(n1.equals(n2));//結果是true根據jdk文檔中的說明,n1與n2指向的對像中的內容是相等的,都是30,故equals比較後結果是true

System.out.println(n1.equals(n3));//結果是false因對象內容不一樣,一個是30一個是31

}

}

這是Integer的實例,如果是其他的比如Double 、Character、Float等也一樣。

(3)注意:對於String(字符串)、StringBuffer(線程安全的可變字符序列)、StringBuilder(可變字符序列)這三個類作進一步的說明。

(a)首先,介紹String的用法,請看下面的實例:

public class TestEquals {

public static void main(String[] args) {

String s1 = "123";

String s2 = "123";

String s3 = "abc ";

String s4 = new String("123");

String s5 = new String("123");

String s6 = new String("abc");

System.out.println(s1 == s2);//( 1)true

System.out.println(s1.equals(s2));//(2)true

System.out.println(s1 == s3);//(3)flase

System.out.println(s1.equals (s3));//(4)flase

System.out.println(s4 == s5);//(5)flase

System.out.println(s4.equals(s5));//(6)true

System .out.println(s4 == s6);//(7)flase

System.out.println(s4.equals(s6));//(8)flase

System.out.println(s1 == s4);/ /(9)false

System.out.println(s1.equals(s4));//(10)true

}

}

答案解釋:s1與s2分別指向由字符串常量”123”創建的對象,在常量池中,只有一個對象,內容為123,有兩個引用s1和s2指向這個對象,故這兩個引用變量所指向的地址是相同的,因而(1)處的運行結果為true,又因為s1.equals (s2)是比較s1和s2所指向的對象的內容是否相等,而我們知道這兩個對象的內容都是字符串常量”123”,故標記(2)處的運行結果是true。

用同樣的方法分析,s1和s3所指向的對像不一樣,內容也不一樣,故標記(3)和(4)處運行結果是false。

再看看s4和s5,這兩個引用變量所指向的對象的內容都是一樣的(內容都是123),但是這兩個對像是用new操作符創建處類的,是在內存中分配兩塊空間給這兩個對象的,因而這兩個對象的內存地址不一樣,故事兩個不同的對象,標記(5)處的s4 == s5運行結果為false,但是內容一樣,故標記(6 )處的s4.equals(s5)運行結果為true。同理,s4和s6所指向的對像地址不同,內容也不相同。故標記(7)(8)處運行結果為false。

s1和s4分別指向兩個不同的對象(之所以這樣稱呼,是因為這兩個對像在內存中的地址不相同,故而對像不相同),故標記為(9)處的s1 == s4運行結果為false,而標記為(10)處的s1.equals(s4)運行結果疑問:乍一看結果,有點驚訝,為什麼不是true呢,不是說equals方法是比較內容的嗎?

解釋:不錯,如果在新類中被覆蓋了equals方法,就可以用來比較內容的。但是在上面的例子中類Value並沒有覆蓋Object中的equals方法,而是繼承了該方法,因此它就是被用來比較地址的,又v1和v2的所指向的對像不相同,故標記(1 )處的v1.equals(v2)運行結果為false,標記為(2)處的v1 == v2運行結果也為false。