Java集合之HashSet


1.HashSet概述:
  HashSet實現Set接口,由哈希表(實際上是一個HashMap實例)支持。它不保證set 的迭代順序;特別是它不保證該順序恆久不變。此類允許使用null元素。HashSet中不允許有重復元素,這是因為HashSet是基於HashMap實現的,HashSet中的元素都存放在HashMap的key上面,而value中的值都是統一的一個private static final Object PRESENT = new Object();。HashSet跟HashMap一樣,都是一個存放鏈表的數組。

  HashSet中add方法調用的是底層HashMap中的put()方法,而如果是在HashMap中調用put,首先會判斷key是否存在,如果key存在則修改value值,如果key不存在這插入這個key-value。而在set中,因為value值沒有用,也就不存在修改value值的說法,因此往HashSet中添加元素,首先判斷元素(也就是key)是否存在,如果不存在這插入,如果存在着不插入,這樣HashSet中就不存在重復值。

2.HashSet的實現:

      對於HashSet而言,它是基於HashMap實現的HashSet底層使用HashMap來保存所有元素,更確切的說,HashSet中的元素,只是存放在了底層HashMap的key上, 而value使用一個static final的Object對象標識。因此HashSet 的實現比較簡單,相關HashSet的操作,基本上都是直接調用底層HashMap的相關方法來完成,HashSet的源代碼如下:

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;

    // 底層使用HashMap來保存HashSet中所有元素。
    private transient HashMap<E,Object> map;
    
    // 定義一個虛擬的Object對象作為HashMap的value,將此對象定義為static final。
    private static final Object PRESENT = new Object();

    /**
     * 默認的無參構造器,構造一個空的HashSet。
      * 
     * 實際底層會初始化一個空的HashMap,並使用默認初始容量為16和加載因子0.75。
      */
    public HashSet() {
        map = new HashMap<E,Object>();
    }

    /**
     * 構造一個包含指定collection中的元素的新set。
      *
     * 實際底層使用默認的加載因子0.75和足以包含指定
     * collection中所有元素的初始容量來創建一個HashMap。
     * @param c 其中的元素將存放在此set中的collection。
     */
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }

    /**
     * 以指定的initialCapacity和loadFactor構造一個空的HashSet。
     *
     * 實際底層以相應的參數構造一個空的HashMap。
     * @param initialCapacity 初始容量。
     * @param loadFactor 加載因子。
     */
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<E,Object>(initialCapacity, loadFactor);
    }

    /**
     * 以指定的initialCapacity構造一個空的HashSet。
     *
     * 實際底層以相應的參數及加載因子loadFactor為0.75構造一個空的HashMap。
     * @param initialCapacity 初始容量。
     */
    public HashSet(int initialCapacity) {
        map = new HashMap<E,Object>(initialCapacity);
    }

    /**
     * 以指定的initialCapacity和loadFactor構造一個新的空鏈接哈希集合。
     * 此構造函數為包訪問權限,不對外公開,實際只是是對LinkedHashSet的支持。
     *
     * 實際底層會以指定的參數構造一個空LinkedHashMap實例來實現。
     * @param initialCapacity 初始容量。
     * @param loadFactor 加載因子。
     * @param dummy 標記。
     */
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
    }

    /**
     * 返回對此set中元素進行迭代的迭代器。返回元素的順序並不是特定的。
     * 
     * 底層實際調用底層HashMap的keySet來返回所有的key。
     * 可見HashSet中的元素,只是存放在了底層HashMap的key上,
     * value使用一個static final的Object對象標識。
     * @return 對此set中元素進行迭代的Iterator。
     */
    public Iterator<E> iterator() {
        return map.keySet().iterator();
    }

    /**
     * 返回此set中的元素的數量(set的容量)。
     *
     * 底層實際調用HashMap的size()方法返回Entry的數量,就得到該Set中元素的個數。
     * @return 此set中的元素的數量(set的容量)。
     */
    public int size() {
        return map.size();
    }

    /**
     * 如果此set不包含任何元素,則返回true。
     *
     * 底層實際調用HashMap的isEmpty()判斷該HashSet是否為空。
     * @return 如果此set不包含任何元素,則返回true。
     */
    public boolean isEmpty() {
        return map.isEmpty();
    }

    /**
     * 如果此set包含指定元素,則返回true。
     * 更確切地講,當且僅當此set包含一個滿足(o==null ? e==null : o.equals(e))
     * 的e元素時,返回true。
     *
     * 底層實際調用HashMap的containsKey判斷是否包含指定key。
     * @param o 在此set中的存在已得到測試的元素。
     * @return 如果此set包含指定元素,則返回true。
     */
    public boolean contains(Object o) {
        return map.containsKey(o);
    }

    /**
     * 如果此set中尚未包含指定元素,則添加指定元素。
     * 更確切地講,如果此 set 沒有包含滿足(e==null ? e2==null : e.equals(e2))
     * 的元素e2,則向此set 添加指定的元素e。
     * 如果此set已包含該元素,則該調用不更改set並返回false。
     *
     * 底層實際將將該元素作為key放入HashMap。
     * 由於HashMap的put()方法添加key-value對時,當新放入HashMap的Entry中key
     * 與集合中原有Entry的key相同(hashCode()返回值相等,通過equals比較也返回true),
     * 新添加的Entry的value會將覆蓋原來Entry的value,但key不會有任何改變,
     * 因此如果向HashSet中添加一個已經存在的元素時,新添加的集合元素將不會被放入HashMap中,
     * 原來的元素也不會有任何改變,這也就滿足了Set中元素不重復的特性。
     * @param e 將添加到此set中的元素。
     * @return 如果此set尚未包含指定元素,則返回true。
     */
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

    /**
     * 如果指定元素存在於此set中,則將其移除。
     * 更確切地講,如果此set包含一個滿足(o==null ? e==null : o.equals(e))的元素e,
     * 則將其移除。如果此set已包含該元素,則返回true
     * (或者:如果此set因調用而發生更改,則返回true)。(一旦調用返回,則此set不再包含該元素)。
     *
     * 底層實際調用HashMap的remove方法刪除指定Entry。
     * @param o 如果存在於此set中則需要將其移除的對象。
     * @return 如果set包含指定元素,則返回true。
     */
    public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }

    /**
     * 從此set中移除所有元素。此調用返回后,該set將為空。
     *
     * 底層實際調用HashMap的clear方法清空Entry中所有元素。
     */
    public void clear() {
        map.clear();
    }

    /**
     * 返回此HashSet實例的淺表副本:並沒有復制這些元素本身。
     *
     * 底層實際調用HashMap的clone()方法,獲取HashMap的淺表副本,並設置到HashSet中。
     */
    public Object clone() {
        try {
            HashSet<E> newSet = (HashSet<E>) super.clone();
            newSet.map = (HashMap<E, Object>) map.clone();
            return newSet;
        } catch (CloneNotSupportedException e) {
            throw new InternalError();
        }
    }
}

3.示例:
接下來看一個示例程序,測試一下自己是否真正掌握了HashSet 集合的功能。

package com.spring.test;

import java.util.HashSet;
import java.util.Set;

class Name   
{   
    private String first;    
    private String last;    
       
    public Name(String first, String last)    
    {    
        this.first = first;    
        this.last = last;    
    }    
  
    public boolean equals(Object o)    
    {    
        if (this == o)    
        {    
            return true;    
        }    
           
        if (o.getClass() == Name.class)    
        {    
            Name n = (Name)o;    
            return n.first.equals(first)    
                && n.last.equals(last);    
        }    
        return false;    
    }    
} 

public class TestSet {
    
    public static void main(String[] args){
        
        Set<Name> s = new HashSet<Name>();   
        s.add(new Name("abc", "123"));   
        System.out.println(   
        s.contains(new Name("abc", "123")));

    }
}

     上面程序中向 HashSet 里添加了一個 new Name("abc", "123") 對象之后,立即通過程序判斷該 HashSet 是否包含一個 new Name("abc", "123") 對象。粗看上去,很容易以為該程序會輸出 true。 實際運行上面程序將看到程序輸出 false,這是因為 HashSet 判斷兩個對象相等的標准除了要求通過 equals() 方法比較返回 true 之外,還要求兩個對象的 hashCode() 返回值相等。而上面程序沒有重寫 Name 類的 hashCode() 方法,兩個 Name 對象的 hashCode() 返回值並不相同,因此 HashSet 會把它們當成 2 個對象處理,因此程序返回 false。
     由此可見,當我們試圖把某個類的對象當成 HashMap 的 key,或試圖將這個類的對象放入 HashSet 中保存時,重寫該類的 equals(Object obj) 方法和 hashCode() 方法很重要,而且這兩個方法的返回值必須保持一致:當該類的兩個的 hashCode() 返回值相同時,它們通過 equals() 方法比較也應該返回 true。通常來說,所有參與計算 hashCode() 返回值的關鍵屬性,都應該用於作為 equals() 比較的標准。 如下程序就正確重寫了 Name 類的 hashCode() 和 equals() 方法,程序如下:

package com.spring.test;

import java.util.HashSet;
import java.util.Set;

class Name   
{   
    private String first;    
    private String last;    
       
    public Name(String first, String last){    
        this.first = first;    
        this.last = last;    
    }    
    
    // 根據 first 判斷兩個 Name 是否相等   
    public boolean equals(Object o){    
        if (this == o){    
            return true;    
        }    
        
        if (o.getClass() == Name.class){    
            Name n = (Name)o;    
            return n.first.equals(first);    
        }    
        return false;    
    } 
    
    // 根據 first 計算 Name 對象的 hashCode() 返回值   
    public int hashCode(){    
        return first.hashCode();    
    }
    
    public String toString(){    
        return"Name[first=" + first + ", last=" + last + "]";    
    } 

} 

public class TestSet {
    
    public static void main(String[] args){
        
        Set<Name> set = new HashSet<Name>();   
        set.add(new Name("abc", "123"));
        set.add(new Name("abc", "456"));
        System.out.println(set);   
    }
}

     上面程序中提供了一個 Name 類,該 Name 類重寫了 equals() 和 toString() 兩個方法,這兩個方法都是根據 Name 類的 first 實例變量來判斷的,當兩個Name 對象的 first 實例變量相等時,這兩個 Name 對象的 hashCode() 返回值也相同,通過 equals() 比較也會返回 true。
     程序主方法先將第一個 Name 對象添加到 HashSet 中,該 Name 對象的 first 實例變量值為"abc",接着程序再次試圖將一個 first 為"abc"的 Name 對象添加到 HashSet 中,很明顯,此時沒法將新的 Name 對象添加到該 HashSet 中,因為此處試圖添加的 Name 對象的 first 也是" abc",HashSet 會判斷此處新增的Name 對象與原有的 Name 對象相同,因此無法添加進入,這時輸出 set 集合時將看到該集合里只包含一個 Name 對象,就是第一個、last 為"123"的 Name 對象。

4.總結:

(1)HashSet 的實現其實非常簡單,它只是封裝了一個 HashMap 對象來存儲所有的集合元素,所有放入 HashSet 中的集合元素實際上由 HashMap 的 key 來保存,而 HashMap 的 value 則存儲了一個 PRESENT,它是一個靜態的 Object 對象。

(2)對於HashSet中保存的對象,請注意正確重寫其equals和hashCode方法,以保證放入的對象的唯一性。

 


免責聲明!

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



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