Skip to content

LinkedHashMap:插入顺序的 HashMap

LinkedHashMap 是什么

一句话:HashMap + 双向链表

它继承自 HashMap,但额外维护了一条双向链表,记录每个 key 的插入顺序(或者访问顺序)。所以它既有 HashMap 的 O(1) 性能,又有顺序。

java
public class LinkedHashMap<K, V> extends HashMap<K, V> {
    // 双向链表的头尾
    transient LinkedHashMap.Entry<K, V> head;
    transient LinkedHashMap.Entry<K, V> tail;

    // true = 访问顺序(LRU),false = 插入顺序(默认)
    final boolean accessOrder;
}

为什么需要双向链表

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

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

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

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

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


LinkedHashMap vs HashMap

java
Map<String, Integer> hash = new HashMap<>();
Map<String, Integer> linked = new LinkedHashMap<>();

hash.put("c", 3); hash.put("a", 1); hash.put("b", 2);
linked.put("c", 3); linked.put("a", 1); linked.put("b", 2);

System.out.println("HashMap: " + hash);       // 无序,如 {a=1, b=2, c=3}
System.out.println("LinkedHashMap: " + linked); // {c=3, a=1, b=2} 严格按插入顺序
特性HashMapLinkedHashMap
去重
插入顺序
get/put 性能O(1)O(1)(稍慢一点)
遍历性能O(n)O(n)(按插入顺序)
内存占用较低较高(额外链表指针)
null key/value

两种模式

插入顺序(默认)

java
LinkedHashMap<String, Integer> insertOrder = new LinkedHashMap<>();
insertOrder.put("first", 1);
insertOrder.put("second", 2);
insertOrder.put("third", 3);
System.out.println(insertOrder); // {first=1, second=2, third=3}

访问顺序(LRU 缓存模式)

传入 accessOrder = true 后,每次 getput 都会把该节点移到链表末尾:

java
LinkedHashMap<String, Integer> accessOrder = new LinkedHashMap<>(16, .75f, true);

accessOrder.put("a", 1);
accessOrder.put("b", 2);
accessOrder.put("c", 3);

System.out.println("初始: " + accessOrder);
// {a=1, b=2, c=3}

accessOrder.get("a"); // 访问 a
System.out.println("访问后: " + accessOrder);
// {b=2, c=3, a=1}  ← a 被移到最后

accessOrder.get("b"); // 访问 b
System.out.println("再次访问: " + accessOrder);
// {c=3, a=1, b=2}  ← b 被移到最后

链表头部是最久未访问的,尾部是最近访问的。


经典应用:LRU 缓存

利用 removeEldestEntry 实现自动淘汰最老元素的缓存:

java
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;

    public LRUCache(int capacity) {
        // accessOrder = true 启用访问顺序
        // removeEldestEntry 在 put 后自动调用
        super(16, .75f, true);
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity; // 超过容量就删除最老的
    }
}

使用示例

java
LRUCache<String, String> cache = new LRUCache<>(3);

cache.put("A", "a");
cache.put("B", "b");
cache.put("C", "c");
System.out.println("初始: " + cache); // {A=a, B=b, C=c}

cache.get("A");                      // 访问 A,A 移到最后
System.out.println("访问 A 后: " + cache); // {B=b, C=c, A=a}

cache.put("D", "d");                  // 添加 D,容量超了
System.out.println("添加 D 后: " + cache); // {C=c, A=a, D=d}
// B 被淘汰了(最久未访问)

典型应用场景

场景 1:去除重复但保持顺序

java
// 去除重复 URL,保留第一次出现的顺序
List<String> urls = Arrays.asList(
    "page1.html", "page2.html", "page1.html", "page3.html", "page2.html"
);
Map<String, String> uniqueUrls = new LinkedHashMap<>();
for (String url : urls) {
    uniqueUrls.putIfAbsent(url, url); // 只保留第一次出现的
}
System.out.println(uniqueUrls.keySet());
// [page1.html, page2.html, page3.html]

场景 2:FIFO 队列

java
LinkedHashMap<String, String> fifo = new LinkedHashMap<>(16, .75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > 5; // 最多保留 5 个
    }
};
// 每次添加超过 5 个,自动删除最老的

场景 3:用户行为记录

java
// 记录用户最近访问的 10 个页面
LinkedHashMap<String, LocalDateTime> recentPages =
    new LinkedHashMap<>(16, .75f, true) {
        @Override
        protected boolean removeEldestEntry(Map.Entry eldest) {
            return size() > 10;
        }
    };

recentPages.put(page, LocalDateTime.now());
// 自动保留最近 10 个页面

性能代价

LinkedHashMap 比 HashMap 稍慢,因为每次插入/删除都要维护双向链表:

java
// 内存开销对比(10 万元素)
HashMap<Integer, Integer> hash = new HashMap<>();
LinkedHashMap<Integer, Integer> linked = new LinkedHashMap<>();

// hash:约 4.8 MB
// linked:约 6.4 MB(双向链表指针额外开销)
操作HashMapLinkedHashMap
getO(1)O(1)
putO(1)O(1)(稍慢)
removeO(1)O(1)(稍慢)
遍历O(n)(无序)O(n)(按顺序)

LinkedHashSet 和 LinkedHashMap 的关系

LinkedHashSet 的底层就是 LinkedHashMap:

java
public class LinkedHashSet<E> extends HashSet<E> {
    public LinkedHashSet() {
        // 底层创建 LinkedHashMap
        super(16, .75f, true);
    }
}

所以 LinkedHashSet 的有序性原理和 LinkedHashMap 完全一样——都是靠双向链表维持顺序。


总结

要点说明
本质HashMap + 双向链表
核心能力去重 + 保持插入顺序 / 访问顺序
两种模式插入顺序(默认)/ 访问顺序(LRU)
LRU 实现accessOrder = true + removeEldestEntry
性能代价比 HashMap 稍慢,内存多约 30%
适用场景去重保序、缓存、按顺序遍历

一句话:LinkedHashMap 是 HashMap 的「有序版本」——用约 30% 的额外内存,换来了遍历的有序性。


相关链接

基于 VitePress 构建