集合(四):LinkedHashMap深度解析——有序哈希与 LRU 缓存的基石

文章目录

    • [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 集合框架中设计非常精妙的一个类,它展示了如何通过组合现有的数据结构(数组+链表)来满足复杂的业务需求。

  1. 双重结构 :继承 HashMap 保证查询效率,内部维护双向链表保证迭代顺序。
  2. accessOrder:决定了是按"插入顺序"还是"访问顺序"维护链表,这是实现 LRU 的开关。
  3. 扩展性 :通过预留的 removeEldestEntry 钩子方法,极大地简化了缓存淘汰策略的实现。
相关推荐
非优秀程序员4 小时前
第04章 邮箱大战,163 与 QQ 的暗战|《互联网产品恩仇录》
java·vue.js·架构
知已难寻5 小时前
Docker 入门:容器技术与 Docker 基础
java·运维·服务器·docker·云原生·容器
文丑颜不良啊7 小时前
LeetCode2481~2500题目解析
java·leetcode
Flynn Liang8 小时前
同一需求三套写法:用 Spring AI、LangChain4j、AgentScope Java 各实现一个带工具调用的 RAG 助手(实战对比)
java·人工智能·大模型·架构设计·spring ai·langchain4j·agentscope
聪明的笨猪猪8 小时前
实际工程问题:AI 生成 Spring Cache(@Cacheable / @CachePut / @CacheEvict)缓存注解踩坑
java·经验分享·面试·ai编程
愿有所息9 小时前
记一次 ARM64 + JDK8 上 fastjson2 的诡异数据错乱
java
秋饼9 小时前
MCP 2026-07-28 无状态协议大改版:Spring AI 2.1 多轮往返请求与网关路由落地
java·ai·技术分享·后端开发
SL_staff10 小时前
JVS-BI 实战:业务人员如何用零代码 ELT + 可视化配置构建实时经营分析闭环
java·spring·全栈
SL_staff11 小时前
企业AI落地难?JVS-AI渐进式实践:安全可控、小切口验证、标准化沉淀与规模化复制
java·开源·aigc
SL_staff12 小时前
JVS数字底座实践:构建企业级统一内容管理平台的技术路径
java·产品