【Java學習筆記】HashSet中加入自定義的類的對象


作者: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 是字符串的長度,^ 表示求冪。(空字符串的哈希值為 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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM