
一句话定义:
LinkedHashMap是HashMap的子类,在保持哈希表 O(1) 快速存取能力的同时,通过维护一个双向链表,实现了可预测的迭代顺序——支持按插入顺序或访问顺序遍历元素。
本文将从“是什么”出发,层层深入到底层数据结构、核心算法机制、两种访问模式的本质差异、LRU 缓存实现原理、性能边界分析,并辅以实战代码与 JDK 源码解读,助你彻底掌握 LinkedHashMap 的设计精髓。
LinkedHashMap?LinkedHashMap 位于 java.util 包中,继承自 HashMap,实现了 Map 接口。它不是对 HashMap 的重写,而是装饰式增强(Decorator Pattern):复用 HashMap 的哈希存储机制,额外叠加一个有序链表结构。
HashMap 虽快,但迭代顺序不可预测(JDK 8+ 虽在特定条件下有序,但不保证)。TreeMap 虽有序,但基于红黑树,时间复杂度为 O(log n),牺牲了性能。LinkedHashMap 在 O(1) 时间复杂度下实现了确定性遍历顺序,完美平衡了性能与有序性。类比理解: 如果说
HashMap是一个无序的快递柜(靠编号快速取件), 那么LinkedHashMap就是在每个格子上贴了一条“时间线标签”,你可以按“放入时间”或“最近使用时间”依次查看所有包裹。
HashMap + 双向链表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)结构正交共存。 即:哈希表用于快速定位,双向链表用于顺序遍历。
transient LinkedHashMap.Entry<K,V> head; // 链表头(最旧)
transient LinkedHashMap.Entry<K,V> tail; // 链表尾(最新)
final boolean accessOrder; // 控制顺序模式head 和 tail 构成链表的首尾哨兵(实际元素在它们之间)。accessOrder 决定链表更新策略(见第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新元素,都会自动追加到双向链表末尾。
LinkedHashMap 的灵魂在于 accessOrder 参数,它决定了链表的维护策略。
模式 | 构造参数 | 行为 | 典型用途 |
|---|---|---|---|
插入顺序 | accessOrder = false(默认) | 元素按 put 顺序排列,get 不改变位置 | 保持配置项顺序、日志记录等 |
访问顺序 | accessOrder = true | 每次 get 或 put(更新值)后,将元素移至链表尾 | LRU 缓存、热点数据追踪 |
当 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 指针。put 更新值也会触发移动!注意:不仅是 get,当 put 一个已存在的 key 并更新 value 时,也会触发 afterNodeAccess(),因为这被视为一次“访问”。
LinkedHashMap 天然适合 LRU?removeEldestEntry() 钩子方法,允许在每次 put 后决定是否淘汰头部元素。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。
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} —— 顺序不变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}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 被移到尾部!操作 | 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的性能损耗几乎可忽略; 当对顺序有强需求时,它是性价比最高的选择。
LinkedHashMap 非线程安全!多线程环境下需外部同步,或使用 Collections.synchronizedMap()。Caffeine 或 Guava Cache。LinkedHashMap 支持序列化,且反序列化后顺序保持不变。LinkedHashSet 的关系LinkedHashSet 内部就是 LinkedHashMap,只是 value 固定为 PRESENT 哨兵对象。LinkedHashSet 也支持插入/访问顺序。LinkedHashMap 的设计哲学LinkedHashMap 是 Java 集合框架中组合优于继承、扩展优于修改的经典范例:
HashMap 的高效哈希机制。removeEldestEntry 使得 LRU 等策略可插拔实现。accessOrder=false 时 get 不操作链表)。何时选用
LinkedHashMap?
accessOrder=true)。TreeMap 以获得更高性能(当不需要排序,只需顺序时)。掌握 LinkedHashMap,不仅是掌握一个集合类,更是理解如何在不破坏原有架构的前提下,优雅地增强功能——这正是优秀软件设计的核心思想。
本文适用于 JDK 8 ~ JDK 21,核心机制稳定未变。