Skip to content

LinkedHashSet:插入顺序的代价

LinkedHashSet 是什么

一句话:HashSet + 插入顺序

它继承自 HashSet,但额外维护了一条双向链表,记录元素的插入顺序。所以它既有 HashSet 的 O(1) 性能,又保持了插入顺序。

java
public class LinkedHashSet<E> extends HashSet<E> {
    // 关键:accessOrder = false → 维护插入顺序
    // 如果 accessOrder = true → 维护访问顺序(LRU 缓存)
    public LinkedHashSet() {
        super(16, .75f, true); // 调用 HashSet 的特殊构造函数
    }
}

第三个参数 true 启用 LinkedHashMap 作为底层存储,而不是普通的 HashMap。


为什么需要双向链表

HashSet 的遍历顺序是 hashCode 分布决定的——无序。

如果想要「插入顺序」,需要额外的数据结构来记录:

┌─────────────────────────────────────────────────────────┐
│ HashSet 的存储(无序)                                     │
├─────────────────────────────────────────────────────────┤
│                                                         │
│   bucket[2] → "A"                                       │
│   bucket[5] → "C" → "B"                                 │
│   bucket[8] → "D"                                       │
│                                                         │
│   遍历顺序:取决于桶的内部顺序,可能是 A, C, B, D           │
│                                                         │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ LinkedHashSet 的存储(有序)                               │
├─────────────────────────────────────────────────────────┤
│                                                         │
│   HashMap 结构(去重):                                  │
│   bucket[2] → "A"                                       │
│   bucket[5] → "C"                                       │
│   bucket[8] → "D"                                       │
│                                                         │
│   双向链表结构(顺序):                                   │
│   head ↔ "A" ↔ "C" ↔ "B" ↔ "D" ↔ tail                │
│                                                         │
│   遍历顺序:按链表,A, C, B, D                            │
│                                                         │
└─────────────────────────────────────────────────────────┘

每个节点同时存在于两个结构中:桶里(用于哈希查找)和链表里(用于顺序遍历)。


LinkedHashSet vs HashSet

java
Set<String> hash = new HashSet<>();
Set<String> linked = new LinkedHashSet<>();

List.of("c", "a", "b").forEach(s -> {
    hash.add(s);
    linked.add(s);
});

System.out.println("HashSet: " + hash);       // 无序,如 [a, b, c]
System.out.println("LinkedHashSet: " + linked); // [c, a, b] 严格按插入顺序
特性HashSetLinkedHashSet
去重
插入顺序
add/contains 性能O(1)O(1)(稍慢一点)
遍历性能O(n)O(n)
内存占用较低较高(额外链表)
null 支持✅ 一个 null✅ 一个 null

LinkedHashSet 的两种模式

插入顺序(默认)

java
LinkedHashSet<String> insertOrder = new LinkedHashSet<>();
insertOrder.add("first");
insertOrder.add("second");
insertOrder.add("third");
System.out.println(insertOrder); // [first, second, third]

访问顺序(LRU 缓存模式)

传入 accessOrder = true 后,访问元素会把元素移到链表末尾:

java
LinkedHashSet<String> accessOrder = new LinkedHashSet<>(16, .75f, true);
accessOrder.add("A");
accessOrder.add("B");
accessOrder.add("C");

accessOrder.contains("A"); // 访问 A
accessOrder.contains("B"); // 访问 B

System.out.println(accessOrder);
// 输出:[C, A, B]  ← 访问过的移到了后面
// 最前面的是最久未访问的

这种模式常用于 LRU 缓存实现

java
// 简单 LRU 缓存示例
LinkedHashSet<Integer> cache = new LinkedHashSet<>(3, .75f, true) {
    @Override
    protected boolean removeEldestEntry eldest) {
        return size() > 3; // 超过 3 个元素就删除最老的
    }
};

cache.add(1);
cache.add(2);
cache.add(3);
cache.add(1); // 重新访问 1
cache.add(4);  // 添加 4,此时 size > 3,最老的(2)被删除
System.out.println(cache); // [1, 3, 4]

性能代价

LinkedHashSet 比 HashSet 慢一点,因为它要多维护一条链表:

java
// 内存开销对比
HashSet<Integer> hash = new HashSet<>();
LinkedHashSet<Integer> linked = new LinkedHashSet<>();

// 添加 10 万元素后
// hash:约 2.4 MB
// linked:约 4.8 MB(双向链表指针额外开销)
操作HashSetLinkedHashSet
addO(1)O(1)(稍慢)
removeO(1)O(1)(稍慢)
containsO(1)O(1)
遍历O(n)O(n)(按插入顺序)

典型应用场景

场景 1:去重且保持顺序

java
// 去除重复但保留第一次出现的顺序
List<String> raw = Arrays.asList("b", "a", "c", "a", "d", "b");
Set<String> unique = new LinkedHashSet<>(raw);
System.out.println(unique); // [b, a, c, d]

场景 2:记录访问顺序

java
// 记录用户访问历史(最近访问的放后面)
LinkedHashSet<String> history = new LinkedHashSet<>();
history.add("page1");
history.add("page2");
history.add("page3");
history.add("page1"); // 重新访问 page1
System.out.println(history); // [page2, page3, page1]

场景 3:LRU 缓存

java
// 基于 LinkedHashSet 的 LRU 缓存
class LRUCache<K> {
    private final int capacity;
    private final LinkedHashMap<K, V> map;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.map = new LinkedHashMap<>(capacity, .75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
                return size() > LRUCache.this.capacity;
            }
        };
    }

    public V get(K key) { return map.get(key); }
    public void put(K key, V value) { map.put(key, value); }
}

总结

要点说明
本质HashSet + 双向链表
核心能力去重 + 保持插入顺序
两种模式插入顺序(默认)/ 访问顺序(LRU)
性能代价比 HashSet 慢一点,内存多一倍
适用场景去重 + 保序、LRU 缓存、访问历史

一句话:LinkedHashSet 用额外的内存换来了顺序——如果你需要插入顺序,它是最佳选择。


相关链接

基于 VitePress 构建