HashSet的特點
- 無序性(存儲元素無序)
- 唯一性(允許使用null)
- 本質上,HashSet底層是通過HashMap來保證唯一性
- HashSet沒有提供
方法,同HashMap一樣,因為Set内部是無序的,是以隻能通過疊代的方式獲得get()
HashSet的繼承體系

HashSet源碼分析
1. 屬性(成員變量)
// HashSet内部使用HashMap來存儲元素,是以本質上是HashMap
private transient HashMap<E,Object> map;
// 虛拟對象,用來作為value放到map中(在HashSet底層的HashMap中,key為要存儲的元素,value統一為PRESENT)
private static final Object PRESENT = new Object();
2. 構造方法
public HashSet() {
map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
// 注意:這裡未用public修飾,主要是給LinkedHashSet使用的
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
構造方法都是調用HashMap對應的構造方法。最後一個構造方法有點特殊,它不是public的,意味着它隻能被同一個包或者子類調用,這是LinkedHashSet專屬的方法。
3. 成員方法
3.1 添加元素add(E e)
// HashSet添加元素的時候,直接調用的是HashMap中的put()方法,
// 把元素本身作為key,把PRESENT作為value,也就是這個map中所有的value都是一樣的。
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
3.2 删除元素remove(Object o)
// HashSet删除元素,直接調用HashMap的remove方法
public boolean remove(Object o) {
// 注意:map的remove傳回是删除元素的value,而Set的remov傳回的是boolean類型
// 如果是null的話說明沒有該元素,如果不是null肯定等于PRESENT
return map.remove(o)==PRESENT;
}
3.3 查找元素contains(Object o)
// Set中沒有get()方法,不像List那樣可以按index擷取元素
public boolean contains(Object o) {
return map.containsKey(o);
}
4. 完整代碼
HashSet是基于HashMap的,是以其源碼較少:
package java.util;
import java.io.InvalidObjectException;
import sun.misc.SharedSecrets;
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
// 内部元素存儲在HashMap中
private transient HashMap<E,Object> map;
// 虛拟元素,用來存到map元素的value中的,沒有實際意義
private static final Object PRESENT = new Object();
// 空構造方法
public HashSet() {
map = new HashMap<>();
}
// 把另一個集合的元素全都添加到目前Set中
// 注意,這裡初始化map的時候是計算了它的初始容量的
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
// 指定初始容量和裝載因子
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
// 隻指定初始容量
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
// LinkedHashSet專用的方法
// dummy是沒有實際意義的, 隻是為了跟上上面那個操持方法簽名不同而已
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
// 疊代器
public Iterator<E> iterator() {
return map.keySet().iterator();
}
// 元素個數
public int size() {
return map.size();
}
// 檢查是否為空
public boolean isEmpty() {
return map.isEmpty();
}
// 檢查是否包含某個元素
public boolean contains(Object o) {
return map.containsKey(o);
}
// 添加元素
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
// 删除元素
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
// 清空所有元素
public void clear() {
map.clear();
}
// 克隆方法
@SuppressWarnings("unchecked")
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(e);
}
}
// 序列化寫出方法
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// 寫出非static非transient屬性
s.defaultWriteObject();
// 寫出map的容量和裝載因子
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());
// 寫出元素個數
s.writeInt(map.size());
// 周遊寫出所有元素
for (E e : map.keySet())
s.writeObject(e);
}
// 序列化讀入方法
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// 讀入非static非transient屬性
s.defaultReadObject();
// 讀入容量, 并檢查不能小于0
int capacity = s.readInt();
if (capacity < 0) {
throw new InvalidObjectException("Illegal capacity: " +
capacity);
}
// 讀入裝載因子, 并檢查不能小于等于0或者是NaN(Not a Number)
// java.lang.Float.NaN = 0.0f / 0.0f;
float loadFactor = s.readFloat();
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
}
// 讀入元素個數并檢查不能小于0
int size = s.readInt();
if (size < 0) {
throw new InvalidObjectException("Illegal size: " +
size);
}
// 根據元素個數重新設定容量
// 這是為了保證map有足夠的容量容納所有元素, 防止無意義的擴容
capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
HashMap.MAXIMUM_CAPACITY);
// 再次檢查某些東西, 不重要的代碼忽視掉
SharedSecrets.getJavaOISAccess()
.checkArray(s, Map.Entry[].class, HashMap.tableSizeFor(capacity));
// 建立map, 檢查是不是LinkedHashSet類型
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
// 讀入所有元素, 并放入map中
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}
// 可分割的疊代器, 主要用于多線程并行疊代處理時使用
public Spliterator<E> spliterator() {
return new HashMap.KeySpliterator<E,Object>(map, 0, -1, 0, 0);
}
}
總結
HashSet内部使用HashMap的key存儲元素,以此來保證元素不重複;
HashSet是無序的,因為HashMap的key是無序的;
HashSet中允許有一個null元素,因為HashMap允許key為null;
HashSet是非線程安全的;
HashSet是沒有get()方法的;
擴充:
當向HashMap中存儲n個元素時,它的初始化容量應指定為:((n/0.75f) + 1),如果這個值小于16,就直接使用16為容量。初始化時指定容量是為了減少擴容的次數,提高效率。
LinkedHashSet分析
package java.util;
// LinkedHashSet繼承自HashSet
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
private static final long serialVersionUID = -2851667679971038690L;
// 傳入容量和裝載因子
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
// 隻傳入容量, 裝載因子預設為0.75
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
// 使用預設容量16, 預設裝載因子0.75
public LinkedHashSet() {
super(16, .75f, true);
}
// 将集合c中的所有元素添加到LinkedHashSet中
// 好奇怪, 這裡計算容量的方式又變了
// HashSet中使用的是Math.max((int) (c.size()/.75f) + 1, 16)
// 這一點有點不得其解, 是作者偷懶?
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
// 可分割的疊代器, 主要用于多線程并行疊代處理時使用
@Override
public Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
}
}
LinkedHashSet繼承自HashSet,它的添加、删除、查詢等方法都是直接用的HashSet的,唯一的不同就是它使用LinkedHashMap存儲元素。
LinkedHashSet是有序的,它是按照插入的順序排序的。
LinkedHashSet是不支援按通路順序對元素排序的,隻能按插入順序排序。
因為,LinkedHashSet所有的構造方法都是調用HashSet的同一個構造方法,如下:
// HashSet的構造方法
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
通過調用LinkedHashMap的構造方法初始化map,如下所示:
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
可以看到,這裡把accessOrder寫死為false了。
是以,LinkedHashSet是不支援按通路順序對元素排序的,隻能按插入順序排序。