LinkedHashMap 源码深度分析(基于 JDK 8)
LinkedHashMap 是 Java 集合框架中兼具 哈希表高效查找 和 双向链表有序迭代 的数据结构,其核心设计是 继承 HashMap 并额外维护双向链表,既保留了 HashMap O (1) 级别的增删改查性能,又解决了 HashMap 迭代无序的问题。同时,通过 accessOrder 开关支持 插入顺序 和 访问顺序(LRU 缓存核心)两种模式。
一、类结构与核心成员
1. 继承关系
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
- 直接继承
HashMap,复用其哈希表(数组 + 链表 / 红黑树)的核心逻辑; - 实现
Map接口,遵循标准 Map 规范。
2. 节点结构(Entry)
LinkedHashMap 重写了 HashMap 的节点类,在 Node 基础上增加 双向链表指针(before/after),同时保留哈希表的 hash/key/value/next 字段:
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);
}
}
- 哈希表维度:
next指针用于解决哈希冲突(链表 / 红黑树); - 双向链表维度:
before(前驱)和after(后继)指针用于维护顺序。
3. 核心成员变量
// 双向链表的头节点(最老节点:插入最早/最久未访问)
transient LinkedHashMap.Entry<K,V> head;
// 双向链表的尾节点(最新节点:插入最晚/最近访问)
transient LinkedHashMap.Entry<K,V> tail;
// 顺序模式开关:false=插入顺序(默认),true=访问顺序(LRU模式)
final boolean accessOrder;
head和tail控制双向链表的遍历和节点增删;accessOrder是 LinkedHashMap 功能分化的核心,默认false(插入顺序)。
二、构造方法
LinkedHashMap 所有构造方法均 调用父类 HashMap 的构造方法,仅额外初始化 accessOrder(默认 false):
// 1. 无参构造:默认初始容量16,负载因子0.75,插入顺序
public LinkedHashMap() {
super();
accessOrder = false;
}
// 2. 指定初始容量
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
// 3. 指定初始容量和负载因子
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
// 4. 传入Map初始化
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false); // 复用HashMap的批量插入逻辑
}
// 5. 关键构造:支持指定顺序模式(accessOrder)
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder; // 唯一能开启LRU模式的构造
}
- 核心差异:前 4 个构造默认
accessOrder=false(插入顺序),第 5 个构造可手动开启accessOrder=true(访问顺序)。
三、核心逻辑:双向链表的维护
LinkedHashMap 的核心价值是 在 HashMap 基础上维护双向链表顺序,所有链表操作均通过重写 HashMap 的 “钩子方法” 和节点创建逻辑实现,不破坏 HashMap 的原有哈希表逻辑。
1. 节点创建:newNode(重写 HashMap 方法)
HashMap 插入节点时会调用 newNode 创建节点,LinkedHashMap 重写该方法,在创建节点后 自动将节点加入双向链表尾部:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<>(hash, key, value, next);
linkNodeLast(p); // 尾插法:将新节点加入双向链表尾部
return p;
}
2. 尾插法:linkNodeLast(私有方法)
负责将新节点插入双向链表尾部,维护 head 和 tail 指针:
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail; // 保存原尾节点
tail = p; // 新节点成为尾节点
if (last == null) { // 原链表为空(首次插入)
head = p; // 新节点同时作为头节点
} else { // 原链表非空
p.before = last; // 新节点的前驱 = 原尾节点
last.after = p; // 原尾节点的后继 = 新节点
}
}
- 插入顺序模式的核心:新节点永远追加到链表尾部,迭代时按插入顺序遍历。
3. 访问后调整:afterNodeAccess(钩子方法)
当 accessOrder=true(访问顺序模式)时,访问节点后需将该节点移到链表尾部(标记为 “最新访问”),这是 LRU 缓存的核心逻辑:
void afterNodeAccess(Node<K,V> e) { // e:被访问的节点
LinkedHashMap.Entry<K,V> last;
// 条件:访问顺序模式 + 被访问节点不是尾节点(无需移动)
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e;
LinkedHashMap.Entry<K,V> b = p.before; // 前驱节点
LinkedHashMap.Entry<K,V> a = p.after; // 后继节点
// 1. 断开p与后继a的连接
p.after = null;
if (b == null) { // p原是头节点(无前驱)
head = a; // 后继a成为新头节点
} else { // p有前驱
b.after = a; // 前驱的后继指向a
}
// 2. 断开p与前驱b的连接
if (a != null) { // a存在(p不是尾节点)
a.before = b; // a的前驱指向b
} else { // a不存在(p原是倒数第二个节点)
last = b; // 原前驱b成为临时尾节点
}
// 3. 将p插入到链表尾部
if (last == null) { // 链表为空(极端情况)
head = p;
} else {
p.before = last; // p的前驱 = 临时尾节点
last.after = p; // 临时尾节点的后继 = p
}
tail = p; // p成为新尾节点
++modCount; // 迭代器快速失败标记
}
}
- 触发时机:调用
get()、getOrDefault()等访问方法时,会先通过 HashMap 的getNode()找到节点,再调用该方法调整顺序。
4. 插入后清理:afterNodeInsertion(钩子方法)
HashMap 插入节点成功后会回调该方法,LinkedHashMap 重写它实现 LRU 缓存的 “淘汰策略”:当 removeEldestEntry() 返回 true 时,删除链表头节点(最老未访问节点):
void afterNodeInsertion(boolean evict) { // evict:是否允许淘汰(put时为true)
LinkedHashMap.Entry<K,V> first;
// 条件:允许淘汰 + 存在头节点(最老节点) + 满足淘汰规则
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true); // 删除头节点(哈希表+链表)
}
}
5. 淘汰规则:removeEldestEntry(可重写方法)
默认返回 false(不淘汰任何节点),要实现 LRU 缓存需重写该方法,定义淘汰条件(如 “容量超过阈值时淘汰最老节点”):
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false; // 默认不淘汰
}
-
示例:重写后实现固定容量的 LRU 缓存:
class LRUCache<K,V> extends LinkedHashMap<K,V> { private final int capacity; public LRUCache(int capacity) { super(capacity, 0.75f, true); // 访问顺序模式 this.capacity = capacity; } @Override protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return size() > capacity; // 容量超限时,淘汰最老节点 } }
6. 删除后调整:afterNodeRemoval(钩子方法)
HashMap 删除节点成功后会回调该方法,LinkedHashMap 重写它以 从双向链表中移除被删除的节点,保证链表结构完整:
void afterNodeRemoval(Node<K,V> e) { // e:被删除的节点
LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e;
LinkedHashMap.Entry<K,V> b = p.before; // 前驱
LinkedHashMap.Entry<K,V> a = p.after; // 后继
// 断开p的连接(帮助GC)
p.before = null;
p.after = null;
// 调整前驱和后继的连接
if (b == null) { // p原是头节点
head = a; // 后继a成为新头节点
} else {
b.after = a;
}
if (a == null) { // p原是尾节点
tail = b; // 前驱b成为新尾节点
} else {
a.before = b;
}
}
四、迭代器与顺序保证
LinkedHashMap 的迭代器 基于双向链表实现,而非哈希表,因此迭代顺序严格遵循链表顺序(插入 / 访问顺序):
keySet()、entrySet()、values()返回的集合迭代器,均通过遍历head → ... → tail实现;- 迭代时间复杂度 O (n),且顺序稳定(HashMap 迭代顺序随机)。
核心迭代逻辑示例(以 entrySet() 为例):
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
private class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
public Iterator<Map.Entry<K,V>> iterator() {
return new LinkedEntryIterator();
}
}
private class LinkedEntryIterator extends LinkedHashIterator implements Iterator<Map.Entry<K,V>> {
public Map.Entry<K,V> next() {
return nextNode(); // 遍历双向链表
}
}
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next; // 下一个节点
LinkedHashMap.Entry<K,V> current; // 当前节点
int expectedModCount; // 快速失败标记
LinkedHashIterator() {
next = head; // 迭代从链表头节点开始
expectedModCount = modCount;
}
public final boolean hasNext() {
return next != null;
}
final LinkedHashMap.Entry<K,V> nextNode() {
LinkedHashMap.Entry<K,V> e = next;
if (modCount != expectedModCount) {
throw new ConcurrentModificationException(); // 快速失败
}
if (e == null) {
throw new NoSuchElementException();
}
current = e;
next = e.after; // 按链表后继节点遍历
return e;
}
public final void remove() { ... } // 调用afterNodeRemoval
}
五、性能与线程安全
1. 性能特点
- 时间复杂度:增删改查均为 O (1)(与 HashMap 一致),仅额外增加双向链表的指针操作(O (1) 开销);
- 空间开销:每个节点比 HashMap 多 2 个指针(
before/after),内存占用略高; - 迭代效率:迭代时无需遍历哈希表空桶,效率比 HashMap 更稳定(尤其当负载因子较高时)。
2. 线程安全
- 与 HashMap 一致,线程不安全:多线程并发修改(如同时 put/remove)可能导致链表结构破坏、迭代器快速失败(
ConcurrentModificationException); - 解决方案:使用
Collections.synchronizedMap(new LinkedHashMap<>())包装,或使用并发容器ConcurrentLinkedHashMap(Guava 提供)。
六、与 HashMap 的核心区别
| 特性 | HashMap | LinkedHashMap |
|---|---|---|
| 底层结构 | 哈希表(数组 + 链表 / 红黑树) | 哈希表 + 双向链表 |
| 迭代顺序 | 无序(随机) | 有序(插入顺序 / 访问顺序) |
| 节点结构 | Node(hash/key/value/next) | Entry(继承 Node + before/after) |
| LRU 支持 | 不支持 | 支持(accessOrder=true) |
| 空间开销 | 较低 | 较高(多 2 个指针) |
| 迭代效率 | 不稳定(需跳过空桶) | 稳定(直接遍历链表) |
七、典型应用场景
- 需要有序迭代的 Map:如日志记录、历史操作记录(插入顺序);
- LRU 缓存:如内存缓存、会话缓存(访问顺序,重写
removeEldestEntry控制容量); - 频繁迭代的场景:比 HashMap 迭代效率更稳定。
总结
LinkedHashMap 的核心设计是 “哈希表保证效率,双向链表保证顺序”:
- 继承 HashMap 复用哈希表的 O (1) 增删改查能力;
- 通过双向链表维护顺序,支持插入 / 访问两种模式;
- 钩子方法(
afterNodeAccess/afterNodeInsertion)实现 LRU 缓存逻辑,扩展性强。

601

被折叠的 条评论
为什么被折叠?



