Java中实现LRU缓存的最简方案是直接使用LinkedHashMap,它通过维护插入顺序或访问顺序的链表,天然支持LRU策略。以下是两种实现方式的对比:
方案1:使用LinkedHashMap(最简实现)
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache extends LinkedHashMap<Integer, Integer> {
private final int capacity;
// 构造函数:设置accessOrder为true,启用访问顺序
public LRUCache(int capacity) {
super(capacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity; // 超过容量时自动删除最老元素
}
};
this.capacity = capacity;
}
public int get(int key) {
return super.getOrDefault(key, -1); // 不存在返回-1
}
public void put(int key, int value) {
super.put(key, value);
}
}
关键点:
LinkedHashMap的构造参数:accessOrder=true:按访问顺序排序(最近访问的元素移到尾部)removeEldestEntry():重写此方法,当大小超过容量时自动删除最老元素
- 时间复杂度:
get/put均为 O(1)
方案2:手动实现(面试常考)
如果你需要在面试中手动实现(不依赖LinkedHashMap),则需用哈希表+双向链表:
import java.util.HashMap;
import java.util.Map;
// 双向链表节点类,用于维护缓存项的访问顺序
class DNode {
int key, value; // 键值对
DNode prev, next; // 前驱和后继节点指针
public DNode(int k, int v) { key = k; value = v; }
}
/**
* LRU缓存实现(最近最少使用策略)
* 使用哈希表+双向链表实现,保证O(1)时间复杂度的get/put操作
* 双向链表维护访问顺序:表头为最近使用的元素,表尾为最久未使用的元素
* 哈希表用于快速定位节点在链表中的位置
*/
public class LRUCache {
private Map<Integer, DNode> map; // 哈希表:键 -> 双向链表节点
private DNode head, tail; // 双向链表的虚拟头尾节点(哨兵节点)
private int capacity; // 缓存容量上限
public LRUCache(int capacity) {
this.capacity = capacity;
map = new HashMap<>();
head = new DNode(0, 0); // 虚拟头节点
tail = new DNode(0, 0); // 虚拟尾节点
head.next = tail; // 初始化链表为空
tail.prev = head;
}
/**
* 获取缓存中的值
* 若键存在,将对应节点移至链表头部(表示最近使用),并返回值
* 若键不存在,返回-1
*/
public int get(int key) {
if (!map.containsKey(key)) return -1;
DNode node = map.get(key);
removeNode(node); // 从当前位置删除
addToHead(node); // 移到链表头部(最近使用)
return node.value;
}
/**
* 向缓存中添加或更新键值对
* 若键已存在,更新值并将节点移至头部
* 若键不存在,创建新节点并添加至头部
* 若添加后超出容量,删除链表尾部节点(最久未使用)
*/
public void put(int key, int value) {
if (map.containsKey(key)) {
removeNode(map.get(key)); // 已存在的键,先删除旧节点
}
DNode newNode = new DNode(key, value);
map.put(key, newNode);
addToHead(newNode); // 新增节点到头部(最近使用)
if (map.size() > capacity) { // 超出容量时
DNode tailNode = removeTail(); // 删除尾部节点
map.remove(tailNode.key); // 同步从哈希表中移除
}
}
/**
* 从链表中删除指定节点
* 调整前驱和后继节点的指针,断开当前节点连接
*/
private void removeNode(DNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
/**
* 将节点添加到链表头部(最近使用位置)
* 操作顺序:先连接新节点与原头节点,再更新虚拟头节点的指针
*/
private void addToHead(DNode node) {
node.next = head.next; // 新节点的后继指向原头节点
node.next.prev = node; // 原头节点的前驱指向新节点
head.next = node; // 虚拟头节点的后继指向新节点
node.prev = head; // 新节点的前驱指向虚拟头节点
}
/**
* 删除链表尾部节点(最久未使用)
* 返回被删除的节点,由调用者负责从哈希表中移除
*/
private DNode removeTail() {
DNode tailNode = tail.prev; // 获取尾节点(虚拟尾节点的前驱)
removeNode(tailNode); // 从链表中删除
return tailNode; // 返回被删除的节点
}
}
对比选择
| 方案 | 优点 | 适用场景 |
|---|---|---|
| LinkedHashMap | 代码仅需10行,简洁高效 | 日常开发、快速实现 |
| 手动实现 | 展示底层原理,面试加分项 | 面试手撕、深入理解LRU机制 |
记忆口诀(LinkedHashMap版)
继承LinkedHashMap,仨参数要写对
accessOrder设为true,删除老元素有回调
get/put直接调父类,LRU缓存就这么简单
关键点:
LinkedHashMap的三个核心参数:初始容量、负载因子、访问顺序- 重写
removeEldestEntry()方法控制删除策略
这种方案避免了手动维护双向链表的复杂性,适合快速实现功能。但面试中若要求“不使用任何库”,则需手动实现版本。