JDK 8 中的 HashSet
是基于 HashMap
实现的 无序集合,它继承自 AbstractSet
并实现了 Set
接口。HashSet
的核心特性是元素唯一(通过 HashMap
键的唯一性保证)、允许 null
元素,但不保证元素的顺序(迭代顺序与插入顺序无关)。
一、HashSet 的核心特性
特性 | 说明 |
---|---|
无序性 | 迭代顺序不保证(与插入顺序或哈希值无关)。 |
元素唯一 | 元素不可重复(依赖 HashMap 键的唯一性)。 |
允许 null 元素 | 允许存储一个 null 元素(HashMap 允许 null 键)。 |
时间复杂度 | 插入、删除、查找等操作的平均时间复杂度为 O(1) (基于哈希表的理想情况)。 |
非线程安全 | 多线程环境下需手动同步(如使用 Collections.synchronizedSet 包装)。 |
二、HashSet 的核心结构
1. 继承与接口
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable
AbstractSet
:提供Set
接口的骨架实现(如size()
、isEmpty()
等方法的默认实现)。Set
:定义集合的核心操作(如add
、remove
、contains
等)。
2. 核心属性
HashSet
内部通过 HashMap
存储元素,元素作为 HashMap
的键,值为静态的 Object
占位符(仅用于标记存在):
// 内部 HashMap 实例(存储元素)
private transient HashMap<E, Object> map;
// 值的占位符(无实际意义,仅用于 HashMap 存储)
private static final Object PRESENT = new Object();
3. 构造方法
HashSet
的构造方法本质是初始化内部的 HashMap
:
// 默认构造方法:初始容量 16,负载因子 0.75
public HashSet() {
map = new HashMap<>();
}
// 指定初始容量的构造方法
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
// 指定初始容量和负载因子的构造方法
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
// 从 Iterable 初始化(添加所有元素)
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size() / .75f) + 1, 16)); // 计算初始容量
addAll(c); // 添加所有元素
}
三、核心方法解析
HashSet
的方法本质上是对内部 HashMap
的封装,核心逻辑依赖 HashMap
的实现。
1. add(E e):添加元素
add
方法通过 HashMap.put(e, PRESENT)
实现,利用 HashMap
键的唯一性保证元素不重复:
public boolean add(E e) {
return map.put(e, PRESENT) == null; // HashMap 的 put 返回旧值(若存在则返回旧值,否则返回 null)
}
- 若元素已存在(
map.put
返回非null
),则返回false
; - 若元素不存在(
map.put
返回null
),则返回true
。
2. contains(Object o):判断元素是否存在
通过 HashMap.containsKey(o)
实现,依赖 HashMap
的哈希查找逻辑:
public boolean contains(Object o) {
return map.containsKey(o); // HashMap 的 containsKey 基于哈希表查找
}
3. remove(Object o):删除元素
通过 HashMap.remove(o)
实现,删除成功返回 true
(若元素存在):
public boolean remove(Object o) {
return map.remove(o) == PRESENT; // HashMap 的 remove 返回旧值(存在则返回 PRESENT,否则返回 null)
}
4. iterator():返回迭代器
HashSet
的迭代器按 HashMap
键的遍历顺序返回元素(无序):
public Iterator<E> iterator() {
return map.keySet().iterator(); // 返回 HashMap 键集的迭代器(无序)
}
5. clear():清空集合
通过 HashMap.clear()
清空所有元素:
public void clear() {
map.clear();
}
四、关键源码细节
1. 序列化与反序列化
HashSet
实现了 Serializable
接口,序列化时会保存 map
字段;反序列化时通过 readObject
方法重建 map
:
// 序列化时写入 map
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
s.defaultWriteObject();
s.writeInt(map.capacity()); // 写入初始容量
s.writeFloat(map.loadFactor()); // 写入负载因子
s.writeInt(map.size()); // 写入元素数量
for (E e : map.keySet()) {
s.writeObject(e); // 写入所有元素
}
}
// 反序列化时读取 map
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
int capacity = s.readInt();
float loadFactor = s.readFloat();
int size = s.readInt();
map = new HashMap<>(capacity, loadFactor); // 重建 HashMap
for (int i = 0; i < size; i++) {
E e = (E) s.readObject();
map.put(e, PRESENT); // 重新添加元素
}
}
2. 克隆(Cloneable)
HashSet
实现了 Cloneable
接口,clone()
方法返回浅拷贝的 HashSet
(内部 map
是原 map
的引用):
public Object clone() {
try {
HashSet<E> newSet = (HashSet<E>) super.clone();
newSet.map = (HashMap<E, Object>) map.clone(); // 浅拷贝 map(键值对引用相同)
return newSet;
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}
五、性能与注意事项
1. 性能特点
- 添加/查找/删除:平均时间复杂度为
O(1)
(哈希冲突少时),最坏情况为O(n)
(哈希冲突严重时,退化为链表)。 - 空间复杂度:需预分配额外空间(负载因子
0.75
是空间与时间的权衡)。
2. 注意事项
- 线程不安全:多线程环境下同时修改
HashSet
可能导致数据不一致(如ConcurrentModificationException
)。解决方案:Set<E> synchronizedSet = Collections.synchronizedSet(new HashSet<>());
- 迭代器的快速失败(Fail-Fast):迭代过程中若集合被修改(非通过迭代器的
remove
方法),会抛出ConcurrentModificationException
。这是通过modCount
字段实现的:// AbstractSet 中的 modCount 字段(继承自 AbstractCollection) protected transient int modCount = 0; // 迭代器检查 modCount private class Itr implements Iterator<E> { int expectedModCount = modCount; // 预期的修改次数 public boolean hasNext() { if (expectedModCount != modCount) { throw new ConcurrentModificationException(); // 快速失败 } // ... } }
null
元素的支持:HashSet
允许存储一个null
元素(HashMap
允许null
键),但多次添加null
会被视为重复。
六、HashSet 与其他 Set 的对比
特性 | HashSet | LinkedHashSet | TreeSet |
---|---|---|---|
底层结构 | HashMap(哈希表) | LinkedHashMap(哈希表+链表) | TreeMap(红黑树) |
顺序性 | 无序 | 插入顺序/访问顺序 | 自然顺序/自定义比较器顺序 |
时间复杂度 | O(1)(平均) | O(1)(平均) | O(log n) |
允许 null | 是 | 是 | 否(自然排序)/ 取决于比较器 |
适用场景 | 快速去重、无顺序需求 | 需要插入/访问顺序 | 需要排序、导航操作 |
七、总结
HashSet
是基于 HashMap
实现的无序集合,核心优势是元素唯一和高效的增删查操作(平均 O(1)
时间复杂度)。其内部通过 HashMap
存储元素(键为集合元素,值为占位符 PRESENT
),排序规则由 HashMap
的哈希函数决定。实际开发中,若需要快速去重或无顺序要求的集合,HashSet
是首选;若需要保持插入顺序,选择 LinkedHashSet
;若需要排序或导航操作,选择 TreeSet
。