文章目录
-
- [1. 核心架构:哈希表与链表的完美融合](#1. 核心架构:哈希表与链表的完美融合)
-
- [1.1 结构思维导图](#1.1 结构思维导图)
- [1.2 节点结构的秘密](#1.2 节点结构的秘密)
- [1.3 结构示意图](#1.3 结构示意图)
- [2. 核心机制:accessOrder 与 顺序维护](#2. 核心机制:accessOrder 与 顺序维护)
-
- [2.1 访问顺序移动流程(LRU 基础)](#2.1 访问顺序移动流程(LRU 基础))
- [2.2 源码解析:afterNodeAccess](#2.2 源码解析:afterNodeAccess)
- [3. 实战:3分钟实现 LRU 缓存](#3. 实战:3分钟实现 LRU 缓存)
-
- [3.1 关键方法:removeEldestEntry](#3.1 关键方法:removeEldestEntry)
- [3.2 LRU 缓存完整代码](#3.2 LRU 缓存完整代码)
- [3.3 LRU 淘汰流程图](#3.3 LRU 淘汰流程图)
在 Java 集合框架中,HashMap 以其高效的查找性能称霸,但它有一个痛点:无序 。当你需要一个既能快速查找,又能保持插入顺序(或访问顺序)的容器时,LinkedHashMap 便是最佳选择。
本文将揭示 LinkedHashMap 如何通过"双重结构"实现有序性,并展示如何利用其独有的机制,用短短几行代码实现一个 LRU (Least Recently Used) 缓存。
1. 核心架构:哈希表与链表的完美融合
LinkedHashMap 继承自 HashMap,它并没有重写哈希算法,而是通过在原有哈希桶的基础上,维护了一条双向链表。
1.1 结构思维导图
LinkedHashMap
继承关系
extends HashMap
implements Map
核心数据结构
哈希表 (数组 + 链表/红黑树): 保证 O(1) 查找
双向链表 (head + tail): 保证迭代顺序
关键属性
accessOrder
false (默认): 插入顺序
true: 访问顺序 (LRU 核心)
Entry 节点
继承 HashMap.Node
新增 before 指针
新增 after 指针
实战应用
LRU 缓存
有序配置加载
1.2 节点结构的秘密
普通的 HashMap 节点只有 next 指针(用于处理哈希冲突)。而 LinkedHashMap 的节点(Entry)增加了 before and after 指针,用于串联起所有元素。
java
// 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);
}
}
1.3 结构示意图
下图展示了 LinkedHashMap 的内存视图。注意看,数据既存在于哈希桶中(垂直方向),也被一条双向链表(水平方向的红色虚线)串联起来。
HashMap Structure
after
before
after
before
after
before
Bucket 0
Bucket 1
Bucket 2
Bucket 3
Key: A
Val: 1
Key: B
Val: 2
Key: C
Val: 3
Key: D
Val: 4
2. 核心机制:accessOrder 与 顺序维护
LinkedHashMap 的构造函数中有一个至关重要的参数:accessOrder。
accessOrder = false(默认) : 插入顺序。新插入的元素放在链表末尾;读取元素不会改变链表顺序。accessOrder = true: 访问顺序 。每当get或put一个已存在的元素时,该元素会被移动到链表的末尾。
2.1 访问顺序移动流程(LRU 基础)
假设我们开启了 accessOrder = true,当前链表为 A <-> B <-> C。当我们调用 get("A") 时:
DoubleLinkedList LinkedHashMap Client DoubleLinkedList LinkedHashMap Client 将 A 从当前位置断开 将 A 移动到链表尾部 get("A") 查找 Key "A" (HashMap逻辑) afterNodeAccess(Node A) 链表变为 B <->> C <->> A return Value A
2.2 源码解析:afterNodeAccess
HashMap 预留了三个回调方法给子类实现,LinkedHashMap 重写了其中的 afterNodeAccess。
java
// 当 accessOrder 为 true 时,将当前节点 e 移到双向链表尾部
void afterNodeAccess(Node<K,V> e) {
LinkedHashMap.Entry<K,V> last;
// 条件:accessOrder 为 true 且 当前节点不是尾节点
if (accessOrder && (last = tail) != e) {
// ... (省略具体的指针断开与重连代码) ...
// 核心逻辑:
// 1. 将 e 从 before 和 after 之间移除
// 2. 将 e 接到 tail 后面
// 3. tail 指向 e
}
}
3. 实战:3分钟实现 LRU 缓存
LRU (Least Recently Used) 是一种常见的缓存淘汰算法:当缓存满时,优先淘汰最近最少使用的数据。
利用 LinkedHashMap 的 accessOrder=true 特性,链表头部 就是"最近最少使用"的元素,尾部是"最近刚被使用"的元素。我们只需要在插入新元素导致容量溢出时,移除链表头部的元素即可。
3.1 关键方法:removeEldestEntry
LinkedHashMap 提供了一个受保护的方法 removeEldestEntry。在每次 put 新元素后,HashMap 会调用这个方法。
- 默认实现返回
false(永不移除)。 - 我们需要重写它:当
size() > capacity时返回true。
3.2 LRU 缓存完整代码
java
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LRUCache(int capacity) {
// 1. 设置初始容量
// 2. loadFactor 设为 0.75
// 3. 关键点:accessOrder 设为 true (按访问顺序排序)
super(capacity, 0.75f, true);
this.capacity = capacity;
}
/**
* 核心钩子方法
* 每次 put 操作后,LinkedHashMap 会调用此方法
* 如果返回 true,则移除链表最老的节点(头部节点)
*/
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
// 测试代码
public static void main(String[] args) {
// 创建一个容量为 3 的 LRU 缓存
LRUCache<String, Integer> cache = new LRUCache<>(3);
cache.put("A", 1);
cache.put("B", 2);
cache.put("C", 3);
System.out.println("初始状态: " + cache); // [A, B, C]
cache.get("A"); // 访问 A,A 变成最新的,移到尾部
System.out.println("访问 A 后: " + cache); // [B, C, A]
cache.put("D", 4); // 插入 D,容量超标,淘汰最老的 B
System.out.println("插入 D 后: " + cache); // [C, A, D]
}
}
3.3 LRU 淘汰流程图
是
否
put 新元素 D
HashMap 插入 D
将 D 链接到链表尾部
调用 removeEldestEntry
size > capacity ?
移除链表头部节点
(最近最少使用的)
结束
LinkedHashMap 是 Java 集合框架中设计非常精妙的一个类,它展示了如何通过组合现有的数据结构(数组+链表)来满足复杂的业务需求。
- 双重结构 :继承
HashMap保证查询效率,内部维护双向链表保证迭代顺序。 - accessOrder:决定了是按"插入顺序"还是"访问顺序"维护链表,这是实现 LRU 的开关。
- 扩展性 :通过预留的
removeEldestEntry钩子方法,极大地简化了缓存淘汰策略的实现。