LinkedHashMap 是 HashMap 的子类,通过双向链表维护键值对的插入顺序或访问顺序,结合了哈希表的快速访问与链表的顺序性。以下是其工作原理及实现细节的深入解析:
一、核心设计
1. 数据结构
-
哈希表 + 双向链表
继承HashMap
的哈希桶结构,并在Entry
节点中增加 前驱(before
)和 后继(after
)指针,形成双向链表: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); } }
-
链表头尾指针
transient LinkedHashMap.Entry<K,V> head; // 链表头(最早插入/访问的节点) transient LinkedHashMap.Entry<K,V> tail; // 链表尾(最新插入/访问的节点)
2. 顺序模式
- 插入顺序(默认)
节点按插入顺序排列,put
或putAll
时新节点追加到链表尾部。 - 访问顺序(
accessOrder=true
)
每次调用get
或put
时,将访问的节点移动到链表尾部(最近访问),适合实现 LRU缓存。
二、关键方法实现
1. 节点维护
-
插入时维护链表
重写newNode
方法,创建带双向指针的Entry
,并链接到链表尾部: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); // 将p链接到链表尾部 return p; }
-
删除时解除链接
重写afterNodeRemoval
方法,在哈希表删除节点后调整链表指针:void afterNodeRemoval(Node<K,V> e) { LinkedHashMap.Entry<K,V> p = (Entry<K,V>)e; LinkedHashMap.Entry<K,V> b = p.before, a = p.after; p.before = p.after = null; if (b == null) head = a; else b.after = a; if (a == null) tail = b; else a.before = b; }
2. 访问顺序调整
当 accessOrder=true
时,访问节点会将其移至链表尾部:
void afterNodeAccess(Node<K,V> e) {
LinkedHashMap.Entry<K,V> last = tail;
if (accessOrder && last != e) {
LinkedHashMap.Entry<K,V> p = (Entry<K,V>)e;
LinkedHashMap.Entry<K,V> 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 tail = b;
linkNodeLast(p); // 将p链接到链表尾部
}
}
三、LRU缓存实现
1. 核心机制
-
淘汰最久未使用节点
设置accessOrder=true
,并覆盖removeEldestEntry
方法定义淘汰策略:protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return size() > MAX_CACHE_SIZE; // 当容量超限时移除最旧节点 }
2. 完整示例
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private static final int MAX_ENTRIES = 100;
public LRUCache() {
super(16, 0.75f, true); // accessOrder=true
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > MAX_ENTRIES;
}
}
四、性能分析
操作 | 时间复杂度 | 说明 |
---|---|---|
get(key) | O(1) | 哈希表访问 + 链表调整(访问顺序模式) |
put(key, val) | O(1) | 哈希表插入 + 链表维护 |
迭代遍历 | O(n) | 按链表顺序遍历,无需遍历哈希桶 |
五、与 HashMap 对比
特性 | LinkedHashMap | HashMap |
---|---|---|
顺序性 | 维护插入/访问顺序 | 无顺序 |
内存占用 | 更高(额外存储双向指针) | 较低 |
迭代性能 | 更优(直接遍历链表) | 需遍历哈希桶 |
应用场景 | 需要顺序性、LRU缓存 | 纯快速键值存储 |
六、实现细节
-
迭代器优化
直接按链表顺序遍历,无需扫描哈希桶空位置:abstract class LinkedHashIterator { LinkedHashMap.Entry<K,V> next = head; // 从头开始 LinkedHashMap.Entry<K,V> current; // ... }
-
序列化处理
仅序列化链表顺序,反序列化时重建哈希表和链表。
七、应用场景
- 保持插入顺序
如需要按插入顺序遍历(如日志记录)。 - LRU缓存
自动淘汰最久未使用的条目。 - 有序键值存储
替代TreeMap
的场景,若无需排序但需保持插入顺序。
八、注意事项
- 线程安全性
非线程安全,多线程环境需使用Collections.synchronizedMap
或ConcurrentHashMap
包装。 - 内存开销
每个节点多存储两个指针,内存占用高于HashMap
。 - 初始化参数
实现LRU时需合理设置初始容量和负载因子,避免频繁扩容和哈希冲突。
LinkedHashMap 通过哈希表与双向链表的结合,在保证高效查找的同时,提供了灵活的顺序管理能力,是Java集合框架中实现有序键值对存储的核心类之一。