一、HashSet 基础认知
1. 定义与特性
- 实现
Set接口,底层依赖 HashMap 存储数据 - 元素不可重复(通过 HashMap Key 唯一性保证)
- 无序:不保证存储和遍历顺序
- 非线程安全
- 允许 存储一个 null 元素
- 无索引,不能通过下标访问
- 底层数据结构:数组 + 链表 + 红黑树(和 HashMap 1.8 完全一致)
2. 继承关系
plaintext
java.lang.Object
↳ java.util.AbstractCollection<E>
↳ java.util.AbstractSet<E>
↳ java.util.HashSet<E>
实现接口:Set<E>, Cloneable, Serializable
二、底层核心原理(面试必问)
1. 底层存储结构
java
运行
// HashSet 源码 ------ 全局变量
private transient HashMap<E, Object> map;
// 固定的静态空对象,作为所有 Key 对应的 Value
private static final Object PRESENT = new Object();
2. 核心设计思想
- HashSet 只存元素值,把值作为 HashMap 的 Key
- Value 统一用一个静态的
PRESENT空对象(节省内存) - 利用 HashMap Key 不可重复 特性,实现 Set 元素去重
- 所有操作本质都是调用 HashMap 对应方法
结论:掌握 HashMap = 掌握 HashSet
三、构造方法(源码)
java
运行
// 1. 空参构造:初始化空 HashMap(默认容量16,负载因子0.75)
public HashSet() {
map = new HashMap<>();
}
// 2. 指定初始容量
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
// 3. 指定容量 + 负载因子
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
// 4. 集合转 HashSet
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
四、核心方法源码解析
1. add (E e) ------ 添加元素(去重核心)
java
运行
public boolean add(E e) {
// 本质:调用 HashMap.put(key, 固定值PRESENT)
// put 返回 null 表示插入成功;返回旧值表示已存在
return map.put(e, PRESENT) == null;
}
执行逻辑:
- 把元素
e作为 Key 存入 HashMap - Value 固定为
PRESENT - 如果 Key 不存在 → put 返回 null → add 返回
true(添加成功) - 如果 Key 已存在 → put 返回旧值 → add 返回
false(添加失败,自动去重)
2. remove (Object o) ------ 删除元素
java
运行
public boolean remove(Object o) {
// 本质:调用 HashMap.remove(key)
return map.remove(o) == PRESENT;
}
3. contains (Object o) ------ 判断是否存在
java
运行
public boolean contains(Object o) {
// 本质:调用 HashMap.containsKey(key)
return map.containsKey(o);
}
4. size() / isEmpty() / clear()
java
运行
public int size() { return map.size(); }
public boolean isEmpty() { return map.isEmpty(); }
public void clear() { map.clear(); }
五、HashSet 元素去重原理(面试高频)
1. 去重流程
- 调用
add()时,先获取元素的 hashCode() - 根据 hash 值计算存储位置
- 如果位置为空 → 直接存储
- 如果位置不为空 → 调用 equals() 逐一比较
- 结果相同 → 判定重复,不插入
- 结果不同 → 链表 / 红黑树追加
2. 必须同时重写 hashCode () 和 equals ()
- 只重写 equals:相同内容对象会被当成不同元素(hashCode 不同)
- 只重写 hashCode:无法精准判断内容相等
- 必须两个都重写,才能保证内容相同的对象判定为重复
示例:自定义对象去重
java
运行
class User {
String name;
int age;
// 必须重写!
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age && Objects.equals(name, user.name);
}
// 必须重写!
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
六、HashSet 遍历方式
1. 迭代器(推荐)
java
运行
Iterator<String> it = set.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
2. 增强 for 循环
java
运行
for (String s : set) {
System.out.println(s);
}
3. forEach(JDK 8+)
java
运行
set.forEach(System.out::println);
七、HashSet vs HashMap(核心对比)
表格
| 特性 | HashSet | HashMap |
|---|---|---|
| 实现接口 | Set | Map |
| 存储数据 | 仅存储元素(Key) | 存储Key-Value 键值对 |
| 底层依赖 | 基于 HashMap 实现 | 数组 + 链表 + 红黑树 |
| 重复处理 | 自动去重 | Key 唯一,Value 可重复 |
| 添加方法 | add(E e) | put(K,V) |
| 内存占用 | 更低(Value 共享同一个对象) | 更高(每个键值对独立存储) |
八、高频面试考点
1. HashSet 如何保证元素不重复?
- 底层使用 HashMap ,将元素作为 Key 存储
- HashMap 通过 hashCode() + equals() 判断 Key 是否重复
- 重复 Key 会覆盖,不新增元素 → 实现 Set 去重
2. HashSet 是有序的吗?
- 无序,不保证插入顺序
- 要有序用 LinkedHashSet
- 要排序用 TreeSet
3. HashSet 线程安全吗?
-
不安全
-
解决方案: java
运行
Set<String> set = Collections.synchronizedSet(new HashSet<>());或用
CopyOnWriteArraySet
4. 可以存储 null 吗?
- 可以,但只能存一个 null
5. 为什么 HashSet 底层用 HashMap,不用其他集合?
- 哈希表查询 / 插入效率 O(1)
- 天然支持去重
- 实现简单,复用 HashMap 所有能力
九、使用场景
-
去重 :快速对列表去重(一行代码)
java
运行
List<String> list = new ArrayList<>(); Set<String> set = new HashSet<>(list); -
判断元素是否存在:contains 效率极高
-
无需重复、无需索引、无需排序的集合场景
十、总结(极简背诵版)
- HashSet = 包装过的 HashMap,只存 Key,Value 共用静态空对象
- 去重核心 :
hashCode() + equals() - 特性:无序、无重复、非线程安全、允许一个 null
- 增删改查:本质都是调用 HashMap 对应方法
- 自定义对象:必须重写两个方法才能正确去重
- 效率:接近 O (1),性能极高