作者:gnuhpc
出處:http://www.cnblogs.com/gnuhpc/
這個話題還是從一個有問題的代碼中引申出來的,原代碼如下:
import java.util.*;
class TreeSetTest
{
public static void main(String[] args)
{
HashSet hs=new HashSet();
Student st1=new Student(1,"zhao1");
Student st2=new Student(1,"zhao1");
hs.add(st1);
hs.add(st2);
System.out.println(hs);
}
}
class Student
{
public Student(int num,String name)
{
this.num=num;
this.name=name;
}
public int hashCode()
{
return new Integer(num).hashCode();
}
public boolean equals(Student st)
{
if (name==st.name) return true;
else return false;
}
public String toString()
{
return "student "+num+" name:"+name;
}
int num;
String name;
}
為什麼st1和st2兩個對象內容完全一樣,卻還能插入到一個set中呢,set不是不能有重複的對象嗎?
這段程式有兩個主要問題,就要先從Java中兩個物件導向的基本含義說起了:
JAVA中的重載overload:
只要是一個類以及其父類裡有的兩個函數有相同的名字但是不同的參數列表 (包括參數類型,參數個數,參數順序3項中的一項或多項)。重載可以在單個類或者兩個具有繼承關係的類中出現。 是實作類別的多態性的一種重要方式。
JAVA中的覆蓋override :
覆蓋只會在類繼承的時候才會出現,覆蓋要求兩個函數的名字和參數列表都完全一樣。
在HashSet判斷是不是重複元素時是使用了equals方法,不過請注意自訂的這個類實際繼承了Object類,而Object類中equals方法的定義如下:
public boolean equals(Object o)
這麼說,這段程式中定義的equals方法是對Object中的equals方法的重載,而不是覆蓋,那麼在HashSet判斷重複元素時,實際調用的就是Object.equals 方法,自然是true。
所以該程式第一個需要修改的地方就是equals方法:我們要的是覆蓋不是重載,為了防止這樣問題,可以加上annotation讓Eclipse自己去判斷。
public boolean equals(Object st)
{
Student tempStudent= (Student) st;
if (name==tempStudent.name) return true;
else return false;
}
另外,該程式段在自訂類的hashCode方法和equals並不一致,前者是用num作為hashCode方法的依據,而後者是用name作為判斷是不是相同的依據。
1)利用HashSet/HashMap/Hashtable類來儲存資料時,都是根據儲存物件的hashcode值來進行判斷是否相同的,在 hashCode中僅在兩個對象有著相同hashCode()的時候才會調用equals方法去比較,因為hashset內部採用對某個數字n進行取餘的 方式對雜湊碼進列區域劃分,也就是說即使雜湊碼不同,他們也可能被劃分在同一個地區。在添加資料時,首先計算hashcode(String 對象的雜湊碼根據以下公式計算: s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] 註:使用 int 演算法,這裡 s[i] 是字串的第 i 個字元,n 是字串的長度,^ 表示求冪。(Null 字元串的雜湊值為 0)),發現在一個地區的才會使用equals方法去一一比較同一地區的對象是否相同,否則直接插入。
| | |
| 地區1 | 地區2 | ……
| | |
這個地區在實現時採用鏈表的方法。
當調用了 HashSet 的 add 方法存放對象 obj , HashSet 會首先調用 obj 的 hasCode 方法得到該對象的雜湊碼, HashSet 會使用一個演算法把它的雜湊碼轉換成一個數組下標,該下標“標記”了 obj 的位置。如果這個位置上的鏈表中沒有元素,那麼就把 obj 對象添加到鏈表上。如果這個位置上的鏈表中已經有了元素,則遍曆這個鏈表,調用 obj 的 equals 方法,判斷 obj 是否和其中的某個元素重複,如果沒有重複的元素,那麼就將 obj 添加到鏈表上;如果有重複的元素,則不會講 obj 對象存入 HashSet 中。
也就是說,根據雜湊表的定義,為了保障相同的對象被放到相同的雜湊地區,則必須滿足條件:有equals() 返回true=> hashCode() 返回true。 因為先判斷的是hashCode的值,換句話說,equals的值為true是hashCode值為true 的充分非必要條件。這樣的話,就不會出現兩個實際相同的對象,僅僅因為不在同一個雜湊地區而被錯誤的加入到雜湊集合中的情況發生了。
2)並且由於鏈表的缺點在於查詢速度慢,所以在我們定義自己的hashCode()和equals()時,為了照顧到雜湊表的效能 ,也要遵循“equals返回false時,hashCode也為false”
綜合上述1)和2)兩點,若hashCode方法和equals不一致則hashCode()和equals()結果沒有任何關係,也就是說 equals返回true時,hashcode()也可能是false的,這個與雜湊表定義中不允許相同的元素的定義不符合,也不符合雜湊表效能最佳化的需 要。
得出的結論是:建議hashCode和equals方法的判斷依據最好是一個,也就是所謂的兩個方法相容。
注意:當一個對象被儲存進Hashset中以後,就不能修改這個對象中那些參與計算雜湊值的欄位了,否則,對象修改後的雜湊值與最初儲存進 hashset對象的雜湊值就不同了,在這種情況下,即使在contains方法使用該對象的當前引用作為參數去檢索hashset集合,也將返回找不到 對象的結果,這會導致無法從hashset集合中單獨刪除當前對象,從而造成記憶體泄露。
執行個體代碼如下:
package testhashcode;
/**
* @author gnuhpc
* email: warmbupt@gmail.com
* blog: http://blog.csdn.net/gnuhpc
* @date 2010-1-13
*/
import java.util.*;
class TreeSetTest
{
public static void main(String[] args)
{
HashSet<Student> hs=new HashSet<Student>();
Student st1=new Student(1,"zhao");
Student st2=new Student(2,"qian");
Student st3=new Student(3,"sun");
hs.add(st1);
hs.add(st2);
hs.add(st3);
System.out.println(hs);
st1.num=4; //可以試著注釋掉這一行看一看結果
hs.remove(st1);
System.out.println(hs);
}
}
class Student
{
public Student(int num,String name)
{
this.num=num;
this.name=name;
}
public int hashCode()
{
return new Integer(num).hashCode();
}
@Override
public boolean equals(Object st)
{
Student tempStudent= (Student) st;
if (num==tempStudent.num) return true;
else return false;
}
public String toString()
{
return "student "+num+" name:"+name;
}
int num;
String name;
}
作者:gnuhpc
出處:http://www.cnblogs.com/gnuhpc/