文章目录
概述
一句话定义:
LinkedHashMap是HashMap的子类,在保持哈希表 O(1) 快速存取能力的同时,通过维护一个双向链表,实现了可预测的迭代顺序——支持按插入顺序或访问顺序遍历元素。
本文将从“是什么”出发,层层深入到底层数据结构、核心算法机制、两种访问模式的本质差异、LRU 缓存实现原理、性能边界分析,并辅以实战代码与 JDK 源码解读,助你彻底掌握 LinkedHashMap 的设计精髓。
1. 什么是 LinkedHashMap?
1.1 基本定位
LinkedHashMap 位于 java.util 包中,继承自 HashMap,实现了 Map 接口。它不是对 HashMap 的重写,而是装饰式增强(Decorator Pattern):复用 HashMap 的哈希存储机制,额外叠加一个有序链表结构。
1.2 为什么需要它?
HashMap虽快,但迭代顺序不可预测(JDK 8+ 虽在特定条件下有序,但不保证)。TreeMap虽有序,但基于红黑树,时间复杂度为 O(log n),牺牲了性能。LinkedHashMap在 O(1) 时间复杂度下实现了确定性遍历顺序,完美平衡了性能与有序性。
类比理解:
如果说HashMap是一个无序的快递柜(靠编号快速取件),
那么LinkedHashMap就是在每个格子上贴了一条“时间线标签”,你可以按“放入时间”或“最近使用时间”依次查看所有包裹。
2. 核心原理:HashMap + 双向链表
2.1 数据结构详解
LinkedHashMap 的核心在于其内部节点类 Entry:
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after; // 新增的双向链表指针
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
- 继承自
HashMap.Node,保留了hash,key,value,next(用于哈希冲突链表)。 - 新增
before和after字段,构成独立于哈希桶的全局双向链表。
关键点:
这个双向链表贯穿所有有效 Entry,与哈希表中的桶(bucket)结构正交共存。
即:哈希表用于快速定位,双向链表用于顺序遍历。
2.2 内部字段
transient LinkedHashMap.Entry<K,V> head; // 链表头(最旧)
transient LinkedHashMap.Entry<K,V> tail; // 链表尾(最新)
final boolean accessOrder; // 控制顺序模式
head和tail构成链表的首尾哨兵(实际元素在它们之间)。accessOrder决定链表更新策略(见第3节)。
2.3 插入流程(put)简析
当调用 put(key, value) 时:
- 调用父类
HashMap.putVal()完成哈希存储。 - 若为新节点(
newNode()被调用),LinkedHashMap重写了该方法:Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<>(hash, key, value, e); linkNodeLast(p); // 将新节点链接到链表尾部 return p; } linkNodeLast()更新tail,并维护before/after指针。
结论:每次
put新元素,都会自动追加到双向链表末尾。
3. 两种访问模式:插入顺序 vs 访问顺序
LinkedHashMap 的灵魂在于 accessOrder 参数,它决定了链表的维护策略。
| 模式 | 构造参数 | 行为 | 典型用途 |
|---|---|---|---|
| 插入顺序 | accessOrder = false(默认) | 元素按 put 顺序排列,get 不改变位置 | 保持配置项顺序、日志记录等 |
| 访问顺序 | accessOrder = true | 每次 get 或 put(更新值)后,将元素移至链表尾 | LRU 缓存、热点数据追踪 |
3.1 访问顺序的实现机制
当 accessOrder = true 时,get 操作会触发 afterNodeAccess() 回调(由 HashMap.get 调用):
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e,
b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
这段代码做了什么?
- 将被访问的节点
e从原链表位置摘除。- 将其重新链接到链表尾部。
- 更新
tail指针。- 整个过程为 O(1),无任何循环或查找。
3.2 put 更新值也会触发移动!
注意:不仅是 get,当 put 一个已存在的 key 并更新 value 时,也会触发 afterNodeAccess(),因为这被视为一次“访问”。
4. 典型应用:LRU 缓存的优雅实现
4.1 为什么 LinkedHashMap 天然适合 LRU?
- 访问顺序模式天然维护了一个“最近使用”队列:head 最久未用,tail 最近使用。
- 提供
removeEldestEntry()钩子方法,允许在每次put后决定是否淘汰头部元素。
4.2 LRU 实现源码剖析
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true); // 注意:accessOrder = true
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity; // 超出容量则返回 true,触发删除
}
}
关键点解析:
- 构造函数参数:
initialCapacity设为缓存容量,避免频繁扩容。 accessOrder = true:启用访问顺序。removeEldestEntry:由put方法内部调用(见HashMap.putVal末尾):
其中会调用afterNodeInsertion(evict); // LinkedHashMap 重写了此方法removeEldestEntry,若返回true,则删除head。
注意陷阱:
若capacity设置过小(如 1),且loadFactor默认 0.75,则哈希表可能在size=1时就扩容,但不影响 LRU 逻辑,因为淘汰只看size() > capacity。
5. 实战代码深度演示
示例 1:插入顺序(默认行为)
var map = new LinkedHashMap<String, Integer>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
System.out.println(map); // {A=1, B=2, C=3}
map.get("A"); // 访问 A
System.out.println(map); // 仍为 {A=1, B=2, C=3} —— 顺序不变
示例 2:访问顺序 + LRU 缓存
var cache = new LRUCache<String, String>(2);
cache.put("1", "one");
cache.put("2", "two");
cache.get("1"); // 访问 1 → 移至尾部
cache.put("3", "three"); // 插入 3,容量超限 → 淘汰最旧的 "2"
System.out.println(cache); // {1=one, 3=three}
示例 3:验证 put 更新也触发移动
var map = new LinkedHashMap<String, String>(4, 0.75f, true);
map.put("A", "old");
map.put("B", "B");
map.put("C", "C");
map.put("A", "new"); // 更新 A 的值
System.out.println(map); // {B=B, C=C, A=new} ← A 被移到尾部!
6. 性能与内存分析
| 操作 | HashMap | LinkedHashMap | 说明 |
|---|---|---|---|
put / get / remove | O(1) 平均 | O(1) 平均 | 链表维护为常数时间开销 |
迭代 (entrySet().iterator()) | O(n),无序 | O(n),严格有序 | LinkedHashMap 直接遍历链表,缓存友好 |
containsValue | O(n),遍历所有桶 | O(n),遍历链表 | 链表遍历通常更快(局部性好) |
| 空间开销 | 每个 Entry 约 32 字节(JVM 64位) | +16 字节(两个引用指针) | 内存增加约 50% |
实测建议:
在元素数量 < 10⁶ 时,LinkedHashMap的性能损耗几乎可忽略;
当对顺序有强需求时,它是性价比最高的选择。
7. 高级话题与注意事项
7.1 线程安全?
LinkedHashMap非线程安全!多线程环境下需外部同步,或使用Collections.synchronizedMap()。- 若需高性能并发 LRU,考虑
Caffeine或Guava Cache。
7.2 序列化行为
LinkedHashMap支持序列化,且反序列化后顺序保持不变。- 链表结构会被完整重建。
7.3 与 LinkedHashSet 的关系
LinkedHashSet内部就是LinkedHashMap,只是 value 固定为PRESENT哨兵对象。- 因此
LinkedHashSet也支持插入/访问顺序。
总结:LinkedHashMap 的设计哲学
LinkedHashMap 是 Java 集合框架中组合优于继承、扩展优于修改的经典范例:
- 复用:完全复用
HashMap的高效哈希机制。 - 扩展:通过双向链表低成本引入顺序语义。
- 开放钩子:
removeEldestEntry使得 LRU 等策略可插拔实现。 - 零冗余:仅在需要时才维护链表(如
accessOrder=false时get不操作链表)。
何时选用
LinkedHashMap?
- 需要 Map 的 O(1) 性能 + 确定遍历顺序。
- 实现 LRU/LFU 缓存(配合
accessOrder=true)。- 替代
TreeMap以获得更高性能(当不需要排序,只需顺序时)。
掌握 LinkedHashMap,不仅是掌握一个集合类,更是理解如何在不破坏原有架构的前提下,优雅地增强功能——这正是优秀软件设计的核心思想。
本文适用于 JDK 8 ~ JDK 21,核心机制稳定未变。
886





