LinkedHashMap 是 HashMap 的子类,在 HashMap 的哈希表结构基础上,通过 维护一个双向链表 实现了 顺序访问特性,支持 插入顺序(Insertion Order) 或 访问顺序(Access Order)(如 LRU 策略)。以下是其底层原理的详细解析:
一、核心数据结构
LinkedHashMap 的底层结构由两部分组成:
-
哈希表
继承自 HashMap,使用数组 + 链表/红黑树存储键值对,实现快速查找(O(1) 时间复杂度)。 -
双向链表
每个节点(Entry
)增加before
和after
指针,将所有节点按顺序串联,形成双向链表。
• 头节点(head):链表最老的节点(插入或访问时间最早)。
• 尾节点(tail):链表最新的节点(插入或访问时间最新)。
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);
}
}
二、顺序维护机制
LinkedHashMap 支持两种顺序模式,通过 accessOrder
参数控制:
• accessOrder = false
(默认):按 插入顺序 维护链表。
• accessOrder = true
:按 访问顺序 维护链表(最近访问的节点移动到链表末尾)。
1. 插入顺序的实现
• 新节点插入链表尾部
重写 newNode()
方法,在创建新节点时将其链接到双向链表的末尾。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
Entry<K,V> p = new Entry<>(hash, key, value, e);
linkNodeLast(p); // 将节点链接到链表尾部
return p;
}
• 链表维护逻辑
private void linkNodeLast(Entry<K,V> p) {
Entry<K,V> last = tail;
tail = p; // 新节点成为尾节点
if (last == null) {
head = p; // 链表为空时,头尾均指向新节点
} else {
p.before = last; // 前驱指向原尾节点
last.after = p; // 原尾节点的后继指向新节点
}
}
2. 访问顺序的实现
• 访问节点后移动至链表尾部
重写 afterNodeAccess()
方法,在节点被访问(如 get
、put
)后调整链表顺序。
void afterNodeAccess(Node<K,V> e) {
Entry<K,V> last = tail;
if (accessOrder && last != e) { // 需要移动且当前节点不是尾节点
Entry<K,V> p = (Entry<K,V>)e;
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;
}
if (last == null) {
head = p;
} else {
p.before = last;
last.after = p;
}
tail = p; // 更新尾节点
}
}
三、LRU 缓存支持
LinkedHashMap 可通过 覆盖 removeEldestEntry()
方法 实现 LRU(最近最少使用)缓存策略。
• 淘汰最旧节点:当元素数量超过容量时,删除链表头部的节点(最久未访问或最早插入的节点)。
示例代码
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int maxCapacity;
public LRUCache(int maxCapacity) {
super(maxCapacity, 0.75f, true); // accessOrder = true
this.maxCapacity = maxCapacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxCapacity; // 容量超限时删除最旧节点
}
}
四、关键方法解析
-
get()
方法
• 调用 HashMap 的getNode()
查找节点。
• 若accessOrder = true
,触发afterNodeAccess()
移动节点到链表尾部。 -
put()
方法
• 继承自 HashMap 的插入逻辑。
• 插入新节点时调用linkNodeLast()
链接到链表尾部。
• 若覆盖已有键值对,触发afterNodeAccess()
(若accessOrder = true
)。 -
remove()
方法
• 继承自 HashMap 的删除逻辑。
• 删除节点后调用afterNodeRemoval()
更新双向链表。
五、性能特点
操作 | 时间复杂度 | 说明 |
---|---|---|
插入(put ) | O(1) | 哈希表插入 + 链表维护 |
查询(get ) | O(1) | 哈希表查找 + 链表调整(访问顺序) |
删除(remove ) | O(1) | 哈希表删除 + 链表维护 |
遍历(forEach ) | O(n) | 直接遍历双向链表,无需处理哈希桶 |
六、与 HashMap 的对比
特性 | HashMap | LinkedHashMap |
---|---|---|
顺序维护 | 无序 | 支持插入顺序或访问顺序 |
内存占用 | 较低 | 较高(额外存储双向链表指针) |
遍历性能 | 需遍历所有桶 | 直接遍历链表,性能更高 |
适用场景 | 通用键值对存储 | 需要顺序访问或实现 LRU 缓存 |
总结
LinkedHashMap 通过 哈希表 + 双向链表 的结构,在保留 HashMap 高效查找的基础上,实现了顺序访问特性。其核心设计包括:
• 双向链表维护顺序:支持插入顺序或访问顺序(LRU)。
• 灵活的扩展性:通过重写 removeEldestEntry()
实现缓存淘汰策略。
• 性能平衡:在少量额外内存开销下,提供更可控的遍历顺序。
适用于需要按顺序迭代或实现缓存淘汰机制的场景(如页面置换、资源池管理等)。