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。
610

被折叠的 条评论
为什么被折叠?



