一、超市存包系统:HashSet 的基本概念
想象你来到超市,存包时会拿到一个存包凭证,上面有一个柜子编号。超市的存包系统有两个神奇特性:
-
相同物品只能存一次(去重)
-
取包时直接用凭证找柜子,速度非常快
这就是 Java 中的HashSet,一个基于哈希表实现的集合,专门用于快速去重和查找。
java
// 创建超市存包系统(HashSet)
HashSet<String> storage = new HashSet<>();
// 存包操作
storage.add("苹果");
storage.add("香蕉");
storage.add("苹果"); // 重复物品,存包失败
// 查看存包情况
System.out.println("储物柜中的物品:" + storage); // 输出:[香蕉, 苹果]
// 检查物品是否存在
boolean hasApple = storage.contains("苹果");
System.out.println("是否存了苹果:" + hasApple); // 输出:true
1.1 存包系统的核心特性
| 特性 | 超市场景比喻 | 技术解释 |
|---|---|---|
| 去重 | 不能存两个相同物品 | 不允许重复元素 |
| 快速查找 | 凭凭证直接找柜子 | 平均 O (1) 时间复杂度 |
| 无序性 | 取包顺序和存包顺序无关 | 不保证元素存储顺序 |
| 基于哈希表 | 柜子编号由物品决定 | 通过哈希函数计算存储位置 |
二、储物柜的内部结构:哈希表的工作原理
超市的存包系统背后有一套智能分配机制:
- 哈希函数:根据物品名称计算柜子编号
- 储物柜数组:存放物品的柜子
- 冲突处理:当多个物品算到同一个柜子时的解决方案
2.1 核心存储结构
java
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable {
// 内部使用HashMap存储,键是物品,值是固定常量
private transient HashMap<E, Object> map;
// 所有键对应的值都是这个常量
private static final Object PRESENT = new Object();
// 构造函数:创建存包系统
public HashSet() {
map = new HashMap<>();
}
// 存包方法
public boolean add(E e) {
// 调用HashMap的put方法,键是物品,值是PRESENT
// 如果返回null,说明存包成功(无重复)
return map.put(e, PRESENT) == null;
}
// 检查物品是否存在
public boolean contains(Object o) {
// 检查HashMap中是否有这个键
return map.containsKey(o);
}
}
2.2 哈希函数与柜子分配
当你存 "苹果" 时,系统会:
-
计算 "苹果" 的哈希值:
hash("苹果") -
用哈希值对柜子总数取模,得到柜子编号:
index = hash % capacity -
将 "苹果" 存到该编号的柜子里
java
// 简化的哈希值计算和柜子分配
int hash = "苹果".hashCode(); // 计算哈希值
int capacity = 16; // 柜子总数
int index = hash & (capacity - 1); // 等价于hash % capacity,计算柜子编号
2.3 处理 "柜子不够" 的情况:哈希冲突
当两个物品算到同一个柜子时(哈希冲突),系统会用 "挂链表" 的方式处理:
plaintext
柜子10: 苹果 → 苹果汁 → 苹果醋
如果链表太长(超过 8 个物品),会升级成 "抽屉柜"(红黑树),提高查找效率。
java
// HashMap中处理哈希冲突的关键代码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// ...
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 处理哈希冲突
if (p.hash == hash && keys相等) {
// 键相同,替换值
} else if (p instanceof TreeNode) {
// 红黑树节点,插入到树中
} else {
// 链表节点,插入到链表末尾
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 链表太长,转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// ...
}
}
}
// ...
}
三、存包系统的日常操作:增删查
3.1 存包 (add):快速判断是否已存
java
// 存包示例
HashSet<String> bag = new HashSet<>();
bag.add("雨伞"); // 存包成功
boolean added = bag.add("雨伞"); // 返回false,因为已存在
System.out.println("存包结果:" + added); // 输出:false
3.2 取包 (remove):快速找到并取出
java
// 取包示例
bag.remove("雨伞"); // 取出雨伞
boolean removed = bag.remove("雨衣"); // 雨衣不存在,返回false
System.out.println("取包结果:" + removed); // 输出:false
3.3 查看 (contains):快速判断是否在柜
java
// 查看示例
boolean hasUmbrella = bag.contains("雨伞"); // 雨伞已取出,返回false
System.out.println("是否有雨伞:" + hasUmbrella); // 输出:false
四、多人同时存包:线程安全问题
当多个顾客同时操作存包系统时,可能会出问题:
java
// 危险!多线程同时操作HashSet
HashSet<String> unsafeBag = new HashSet<>();
Thread customer1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
unsafeBag.add("物品" + i);
}
});
Thread customer2 = new Thread(() -> {
for (int i = 50; i < 150; i++) {
unsafeBag.remove("物品" + i);
}
});
customer1.start();
customer2.start();
// 可能抛出ConcurrentModificationException或数据错乱
安全的存包系统:线程安全方案
-
加锁方案:给存包系统加一把大锁
java
// 使用Collections.synchronizedSet包装
Set<String> safeBag = Collections.synchronizedSet(new HashSet<>());
// 存包时需要手动加锁
synchronized (safeBag) {
safeBag.add("重要物品");
}
-
并发专用方案:使用 ConcurrentHashMap 实现的集合
java
import java.util.concurrent.ConcurrentHashMap;
// 自定义线程安全的HashSet
class ThreadSafeHashSet<E> {
private final ConcurrentHashMap<E, Boolean> map;
public ThreadSafeHashSet() {
map = new ConcurrentHashMap<>();
}
public boolean add(E e) {
return map.putIfAbsent(e, true) == null;
}
public boolean contains(E e) {
return map.containsKey(e);
}
}
五、存包系统的性能分析
5.1 正常情况下的速度:非常快!
| 操作 | 时间复杂度 | 超市场景比喻 |
|---|---|---|
| 存包 (add) | O(1) | 直接找到柜子存包 |
| 取包 (remove) | O(1) | 直接找到柜子取包 |
| 查看 (contains) | O(1) | 直接查看柜子是否有物品 |
5.2 特殊情况:柜子太挤时会变慢
当很多物品算到同一个柜子时(哈希冲突严重),操作时间会变长:
- 链表很长时:O (n)
- 红黑树时:O (log n)
六、HashSet 的典型应用场景
6.1 数据去重:学生点名系统
java
// 记录出勤学生,自动去重
HashSet<String> presentStudents = new HashSet<>();
// 学生签到
presentStudents.add("张三");
presentStudents.add("李四");
presentStudents.add("张三"); // 重复签到,无效
// 统计出勤人数
System.out.println("出勤人数:" + presentStudents.size());
6.2 快速查找:黑名单系统
java
// 网站黑名单系统
HashSet<String> blacklist = new HashSet<>();
// 添加黑名单用户
blacklist.add("恶意用户1");
blacklist.add("恶意用户2");
// 检查用户是否在黑名单
boolean isBlocked = blacklist.contains("恶意用户1");
System.out.println("是否禁止访问:" + isBlocked);
6.3 交集、并集运算:数据分析
java
// 商品分类分析
HashSet<String> electronics = new HashSet<>(Arrays.asList("手机", "电脑", "平板"));
HashSet<String> hotSales = new HashSet<>(Arrays.asList("手机", "耳机", "电视"));
// 求交集:既属于电子产品又热销的商品
HashSet<String> intersection = new HashSet<>(electronics);
intersection.retainAll(hotSales);
System.out.println("热销电子产品:" + intersection);
七、总结:HashSet 的核心秘密
通过超市存包的比喻,我们理解了HashSet的核心原理:
-
基于
HashMap实现,元素作为键,值用固定常量PRESENT -
利用哈希函数快速定位存储位置,平均 O (1) 操作效率
-
哈希冲突时用链表或红黑树处理,保证性能
-
非线程安全,多线程场景需额外处理
-
适用于需要快速去重和查找的场景
记住这个存包系统的故事,下次遇到需要去重或快速查找的问题,就知道该请HashSet出场了!