深入原始碼分析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。