1. 程式人生 > >深入原始碼分析HashSet

深入原始碼分析HashSet

前面我們花了一定的篇幅學習了HashMap的一些底層原理,以及簡單瞭解了HashSet和HashMap兩種集合的淵源,現在我們從HashSet原始碼入手,來學習HashSet更細節的地方。

對於HashSet而言,它是基於HashMap實現的。HashSet底層採用HashMap來儲存元素,因此HashSet底層其實比較簡單。

package java.util;

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static
final long serialVersionUID = -5024744406713321676L; // HashSet是通過map(HashMap物件)儲存內容的 private transient HashMap<E,Object> map; // 定義一個虛擬的Object PRESENT是向map中插入key-value對應的value // 因為HashSet中只需要用到key,而HashMap是key-value鍵值對; // 所以,向map中新增鍵值對時,鍵值對的值固定是PRESENT private static final Object PRESENT = new
Object(); // 預設建構函式 底層建立一個HashMap public HashSet() { // 呼叫HashMap的預設建構函式,建立map map = new HashMap<E,Object>(); } // 帶集合的建構函式 public HashSet(Collection<? extends E> c) { // 建立map。 // 為什麼要呼叫Math.max((int) (c.size()/.75f) + 1, 16),從 (c.size()/.75f) + 1 和 16 中選擇一個比較大的樹呢?
// 首先,說明(c.size()/.75f) + 1 // 因為從HashMap的效率(時間成本和空間成本)考慮,HashMap的載入因子是0.75。 // 當HashMap的“閾值”(閾值=HashMap總的大小*載入因子) < “HashMap實際大小”時, // 就需要將HashMap的容量翻倍。 // 所以,(c.size()/.75f) + 1 計算出來的正好是總的空間大小。 // 接下來,說明為什麼是 16 。 // HashMap的總的大小,必須是2的指數倍。若建立HashMap時,指定的大小不是2的指數倍; // HashMap的建構函式中也會重新計算,找出比“指定大小”大的最小的2的指數倍的數。 // 所以,這裡指定為16是從效能考慮。避免重複計算。 map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16)); // 將集合(c)中的全部元素新增到HashSet中 addAll(c); } // 指定HashSet初始容量和載入因子的建構函式 public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<E,Object>(initialCapacity, loadFactor); } // 指定HashSet初始容量的建構函式 public HashSet(int initialCapacity) { map = new HashMap<E,Object>(initialCapacity); } HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor); } // 返回HashSet的迭代器 public Iterator<E> iterator() { // 實際上返回的是HashMap的“key集合的迭代器” return map.keySet().iterator(); } //呼叫HashMap的size()方法返回Entry的數量,得到該Set裡元素的個數 public int size() { return map.size(); } //呼叫HashMap的isEmpty()來判斷HaspSet是否為空 //HashMap為null。對應的HashSet也為空 public boolean isEmpty() { return map.isEmpty(); } //呼叫HashMap的containsKey判斷是否包含指定的key //HashSet的所有元素就是通過HashMap的key來儲存的 public boolean contains(Object o) { return map.containsKey(o); } // 將元素(e)新增到HashSet中,也就是將元素作為Key放入HashMap中 public boolean add(E e) { return map.put(e, PRESENT)==null; } // 刪除HashSet中的元素(o),其實是在HashMap中刪除了以o為key的Entry public boolean remove(Object o) { return map.remove(o)==PRESENT; } //清空HashMap的clear方法清空所有Entry public void clear() { map.clear(); } // 克隆一個HashSet,並返回Object物件 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(); } } // java.io.Serializable的寫入函式 // 將HashSet的“總的容量,載入因子,實際容量,所有的元素”都寫入到輸出流中 private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { // Write out any hidden serialization magic s.defaultWriteObject(); // Write out HashMap capacity and load factor s.writeInt(map.capacity()); s.writeFloat(map.loadFactor()); // Write out size s.writeInt(map.size()); // Write out all elements in the proper order. for (Iterator i=map.keySet().iterator(); i.hasNext(); ) s.writeObject(i.next()); } // java.io.Serializable的讀取函式 // 將HashSet的“總的容量,載入因子,實際容量,所有的元素”依次讀出 private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { // Read in any hidden serialization magic s.defaultReadObject(); // Read in HashMap capacity and load factor and create backing HashMap int capacity = s.readInt(); float loadFactor = s.readFloat(); map = (((HashSet)this) instanceof LinkedHashSet ? new LinkedHashMap<E,Object>(capacity, loadFactor) : new HashMap<E,Object>(capacity, loadFactor)); // Read in size int size = s.readInt(); // Read in all elements in the proper order. for (int i=0; i<size; i++) { E e = (E) s.readObject(); map.put(e, PRESENT); } } }

從上述HashSet原始碼可以看出,它其實就是一個對HashMap的封裝而已。所有放入HashSet中的集合元素實際上由HashMap的key來儲存,而HashMap的value則儲存了一個PRESENT,它是一個靜態的Object物件。

HashSet的絕大部分方法都是通過呼叫HashMap的方法來實現的,因此HashSet和HashMap兩個集合在實現本質上是相同的。

根據HashMap的一個特性: 將一個key-value對放入HashMap中時,首先根據key的hashCode()返回值決定該Entry的儲存位置,如果兩個key的hash值相同,那麼它們的儲存位置相同。如果這個兩個key的equalus比較返回true。那麼新新增的Entry的value會覆蓋原來的Entry的value,key不會覆蓋。因此,如果向HashSet中新增一個已經存在的元素,新新增的集合元素不會覆蓋原來已有的集合元素。

現在我們通過一個實際的例子來看看是否真正理解了HashMap和HashSet儲存元素的細節:

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 HashSetTest
{
    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判斷兩個物件相等的標準是想通過hashCode()方法計算出其hash值,當hash值相同的時候才繼續判斷equals()方法。而如上程式我們並沒有重寫hashCode()方法。所以兩個Name類的hash值並不相同,因此HashSet會把其當成兩個物件來處理。

所以,當我們要將一個類作為HashMap的key或者儲存在HashSet的時候。通過重寫hashCode()和equals(Object object)方法很重要,並且保證這兩個方法的返回值一致。當兩個類的hashCode()返回一致時,應該保證equasl()方法也返回true。當給上述Name類增加如下方法:

public void hashCode(){

return first.hashCode()+last.hashCode();
}

此時我們測試的方法會返回true。