一、超市排队的奥秘:LinkedHashSet 的基本概念
想象你在超市购物,推着一辆神奇的购物车 —— 它会自动去重,而且记住你放入商品的顺序。这就是 Java 中的LinkedHashSet,一个既可以去重又能保持插入顺序的神奇集合。与普通购物车(HashSet)不同,它会像排队一样记住你放入商品的先后顺序,让你在结账时能按放入顺序查看商品。
1.1 购物车的神奇特性
-
去重功能:不能放入相同商品,就像不能买两件同样的牛奶
-
有序性:取出商品时按放入顺序排列,而非随机
-
底层实现:像超市的智能货架系统,用哈希表快速查找商品,用双向链表记录放入顺序
java
// 创建一个超市购物车(LinkedHashSet)
LinkedHashSet<String> shoppingCart = new LinkedHashSet<>();
// 放入商品(自动去重)
shoppingCart.add("牛奶");
shoppingCart.add("鸡蛋");
shoppingCart.add("面包");
shoppingCart.add("牛奶"); // 重复,不会加入
// 按放入顺序遍历购物车
System.out.println("购物车商品:");
for (String item : shoppingCart) {
System.out.println(item);
}
// 输出:牛奶、鸡蛋、面包
1.2 与其他购物车的区别
| 购物车类型 | 特点 | 超市场景类比 |
|---|---|---|
| LinkedHashSet | 有序去重 | 按放入顺序记录的购物车 |
| HashSet | 无序去重 | 随意堆放的普通购物车 |
| TreeSet | 有序去重且排序 | 按商品名称字母排序的购物车 |
二、购物车的内部结构:哈希表 + 双向链表
超市的智能购物车内部有两个核心系统:
- 哈希检索系统:快速查找商品是否已存在
- 排队记录系统:用双向链表记录放入顺序
2.1 购物车的核心组件
java
// LinkedHashSet的核心结构(简化示意)
public class LinkedHashSet<E> extends HashSet<E> {
// 内部使用LinkedHashMap,键是商品,值是固定常量
private transient LinkedHashMap<E, Object> map;
private static final Object PRESENT = new Object(); // 固定值
// 构造函数:创建带初始容量的购物车
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, 0.75f, true); // 创建LinkedHashMap
}
// 添加商品的核心方法
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
}
// HashSet的关键构造函数
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
2.2 双向链表的排队记录
每个商品在购物车中不仅存在于哈希表中,还被双向链表连接,形成排队顺序:
plaintext
牛奶 <-> 鸡蛋 <-> 面包
- 每个节点记录前一个商品和后一个商品的引用
- 头节点是第一个放入的商品,尾节点是最后放入的
- 遍历时从链表头开始,按顺序取出商品
三、购物车操作:添加、删除与查看
3.1 放入商品:add 方法
当你往购物车放牛奶时,购物车会:
-
用哈希表检查是否已存在牛奶
-
若不存在,加入哈希表,并添加到链表末尾
-
维护链表顺序,确保按放入顺序排列
java
// 添加商品的源码模拟
public boolean add(E e) {
// 调用LinkedHashMap的put方法,键是商品,值是PRESENT
return map.put(e, PRESENT) == null;
}
// LinkedHashMap的put方法简化逻辑
public V put(K key, V value) {
// 计算哈希值确定位置
int hash = hash(key);
int index = indexFor(hash, table.length);
// 检查该位置是否已有商品
Node<K,V> first = table[index];
if (first == null) {
// 空位,直接添加
table[index] = newNode(hash, key, value, null);
} else {
// 处理哈希冲突(链表或红黑树)
// ...
}
// 维护双向链表:将新节点添加到链表末尾
linkNodeLast(newNode);
return null;
}
// 维护双向链表的方法
private void linkNodeLast(Node<K,V> p) {
Node<K,V> last = tail;
tail = p;
if (last == null) {
head = p; // 第一个节点
} else {
p.before = last;
last.after = p; // 新节点接到链表末尾
}
}
3.2 移除商品:remove 方法
当你决定不买鸡蛋时,购物车会:
-
在哈希表中找到鸡蛋的位置
-
从链表中切断前后连接,移除鸡蛋
-
调整哈希表和链表结构
java
public boolean remove(Object o) {
return map.remove(o) == PRESENT;
}
// LinkedHashMap的remove方法简化逻辑
public V remove(Object key) {
Node<K,V> e = removeNode(hash(key), key, null, false, true);
return e == null ? null : e.value;
}
// 移除节点并调整链表
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 找到节点后,调整双向链表
if (e != null) {
if (e.before != null)
e.before.after = e.after;
else
head = e.after;
if (e.after != null)
e.after.before = e.before;
else
tail = e.before;
// ...
}
return e;
}
3.3 查看购物车:iterator 方法
遍历购物车时,会按链表顺序依次取出商品:
java
// 遍历购物车的源码逻辑
public Iterator<E> iterator() {
return map.keySet().iterator();
}
// LinkedHashMap的keySet迭代器
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new LinkedKeySet();
keySet = ks;
}
return ks;
}
// 链表迭代器,按before-after顺序遍历
class LinkedKeyIterator extends LinkedHashIterator implements Iterator<K> {
public K next() { return nextNode().key; }
}
class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next = head; // 从链表头开始
public boolean hasNext() {
return next != null;
}
final LinkedHashMap.Entry<K,V> nextNode() {
LinkedHashMap.Entry<K,V> e = next;
next = e.after; // 移到下一个节点
return e;
}
}
四、购物车的性能与线程安全
4.1 性能特点
| 操作 | 时间复杂度 | 超市场景解释 |
|---|---|---|
| 添加 (add) | O(1) | 快速检查并放入购物车 |
| 移除 (remove) | O(1) | 快速找到并移除商品 |
| 包含 (contains) | O(1) | 快速检查商品是否在购物车 |
| 遍历 (iterator) | O(n) | 按顺序查看所有商品 |
4.2 多顾客同时使用的问题
当多个顾客同时操作同一购物车时,可能出现混乱:
java
// 危险!多线程同时操作LinkedHashSet
LinkedHashSet<String> cart = new LinkedHashSet<>();
Thread customer1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
cart.add("商品" + i);
}
});
Thread customer2 = new Thread(() -> {
for (int i = 50; i < 150; i++) {
cart.remove("商品" + i);
}
});
customer1.start();
customer2.start();
// 可能抛出ConcurrentModificationException或数据错乱
4.3 安全的购物车方案
使用同步包装或并发容器:
java
// 方案1:使用Collections.synchronizedSet
Set<String> safeCart = Collections.synchronizedSet(new LinkedHashSet<>());
// 方案2:自定义线程安全的LinkedHashSet
class ThreadSafeLinkedHashSet<E> {
private final Set<E> set;
public ThreadSafeLinkedHashSet() {
set = Collections.synchronizedSet(new LinkedHashSet<>());
}
public synchronized boolean add(E e) {
return set.add(e);
}
public synchronized boolean remove(Object o) {
return set.remove(o);
}
// 其他方法类似...
}
五、LinkedHashSet 的应用场景
5.1 网页访问记录
记录用户访问过的网页,去重并保持访问顺序:
java
LinkedHashSet<String> visitedPages = new LinkedHashSet<>();
// 用户访问网页
visitedPages.add("https://home.com");
visitedPages.add("https://product.com");
visitedPages.add("https://cart.com");
visitedPages.add("https://home.com"); // 重复,忽略
// 显示访问历史
System.out.println("最近访问记录:");
for (String page : visitedPages) {
System.out.println(page);
}
5.2 音乐播放列表
创建去重但保持添加顺序的播放列表:
java
LinkedHashSet<String> playlist = new LinkedHashSet<>();
playlist.add("歌曲A");
playlist.add("歌曲B");
playlist.add("歌曲A"); // 重复,不添加
playlist.add("歌曲C");
System.out.println("播放顺序:");
for (String song : playlist) {
System.out.println(song);
}
5.3 任务执行记录
记录任务执行顺序,去重并按添加顺序执行:
java
LinkedHashSet<String> taskList = new LinkedHashSet<>();
taskList.add("任务1");
taskList.add("任务2");
taskList.add("任务1"); // 重复
taskList.add("任务3");
System.out.println("任务执行顺序:");
for (String task : taskList) {
System.out.println(task);
}
六、总结:LinkedHashSet 的核心秘密
通过超市购物车的比喻,我们理解了LinkedHashSet的核心原理:
-
基于
LinkedHashMap实现,键是元素,值是固定常量 -
用哈希表实现快速去重,用双向链表维护插入顺序
-
添加、删除、查找操作平均时间复杂度 O (1),遍历 O (n)
-
非线程安全,多线程场景需额外处理
-
适用于需要去重并保持顺序的场景,如访问记录、播放列表
记住这个购物车的故事,下次需要有序去重时,你就知道该请LinkedHashSet出场了!