当前位置: 首页 > news >正文

LinkedHashMap 访问顺序模式

0. 背景

LinkedHashMap 构造参数 accessOrder=true 时,任何一次 get/put/getOrDefault 都会把被访问节点移到双向链表末尾,以保证 LRU 语义。
但在实际业务中,经常出现“只想拿到第一个插入的元素,却又不想破坏原有 LRU 顺序”的需求。本文给出三种零副作用(或低副作用)的解决方案,并附可直接落地的源码。


1. LinkedHashMap 关键行为回顾

构造方法顺序语义是否移动节点
new LinkedHashMap<>()插入顺序
new LinkedHashMap<>(16,0.75f,true)访问顺序get/put 均会移动

移动规则

  1. 访问(get)某节点 ⇒ 把该节点移到链表尾

  2. 插入已存在 key ⇒ 先删旧节点,再尾插新节点

  3. 迭代器顺序 = 链表顺序


2. 问题复现

public static void main(String[] args) {Map<String, String> map = new LinkedHashMap<>(16, 0.75f, true);map.put("A", "1");map.put("B", "2");map.get("A");          // 触发访问,A 被移到最后System.out.println(map.keySet()); // 输出 [B, A]
}

需求:在 不破坏 [B, A] 顺序的前提下,拿到 key="A" 的 value。


3. 解决方案

3.1 方案 A:只读迭代器(零副作用)

适用场景:仅想知道“第一个 key/entry”是什么,而不需要 value。

// 取第一个 key
String firstKey = map.keySet().iterator().next();// 取第一个 entry
Map.Entry<String, String> firstEntry = map.entrySet().iterator().next();

注意:只要不去调用 map.get(...),链表就不会被修改。


3.2 方案 B:反射“静默 get”(零副作用,O(1))

适用场景:必须根据 key 拿 value,但又不希望该节点被“访问后移”。

实现原理:直接定位 HashMap 内部 Node[] table,跳过 afterNodeAccess 钩子。

import java.lang.reflect.Field;
import java.util.LinkedHashMap;public final class LinkedHashMapSilentGet {/*** 读取 value,但不触发 LRU 移动*/@SuppressWarnings("unchecked")public static <K, V> V getQuietly(LinkedHashMap<K, V> map, K key) {try {Field tableField = java.util.HashMap.class.getDeclaredField("table");tableField.setAccessible(true);Object[] tab = (Object[]) tableField.get(map);if (tab == null) return null;int hash = (key == null) ? 0 : (key.hashCode() ^ (key.hashCode() >>> 16));int index = (tab.length - 1) & hash;// 遍历该桶下的链表/树for (Object node = tab[index]; node != null; ) {Class<?> nodeClz = node.getClass();Field keyField = nodeClz.getDeclaredField("key");Field valueField = nodeClz.getDeclaredField("value");Field hashField = nodeClz.getDeclaredField("hash");keyField.setAccessible(true);valueField.setAccessible(true);hashField.setAccessible(true);int h = hashField.getInt(node);K k = (K) keyField.get(node);if (h == hash && (key == k || (key != null && key.equals(k)))) {return (V) valueField.get(node);}// next 字段Field nextField = nodeClz.getDeclaredField("next");nextField.setAccessible(true);node = nextField.get(node);}} catch (Exception ignore) {// 降级:正常 get(会移动节点)return map.get(key);}return null;}/* ---------- 测试 ---------- */public static void main(String[] args) {LinkedHashMap<String, String> map = new LinkedHashMap<>(16, 0.75f, true);map.put("A", "1");map.put("B", "2");System.out.println("Before quiet get: " + map.keySet()); // [A, B]String val = getQuietly(map, "A");                       // 不移动System.out.println("value = " + val);                    // 1System.out.println("After  quiet get: " + map.keySet()); // [A, B]}
}

注:JDK 9+ 模块系统需加 --add-opens java.base/java.util=ALL-UNNAMED


3.3 方案 C:快照副本(简单暴力)

适用场景:数据量不大、只读频繁、可接受额外内存。

// 1. 生成当前顺序的快照(插入顺序)
Map<String, String> snap = new LinkedHashMap<>(map);// 2. 在快照里随便 get,不影响原 map
String firstKey = snap.keySet().iterator().next();
String valOfA   = snap.get("A");   // 不会动原 map

4. 三种方案对比

维度方案 A 迭代器方案 B 静默 get方案 C 快照
是否移动节点
能否拿 value
时间复杂度O(1)O(1)O(n) 复制 + O(1)
额外内存00O(n)
代码复杂度最低较高(反射)最低
JDK 限制需开放模块

5. 常见坑 & 建议

  1. 不要在 foreach 里直接 map.remove
    会抛 ConcurrentModificationException,请用迭代器自身删除

    for (Iterator<Map.Entry<String,String>> it = map.entrySet().iterator(); it.hasNext();) {Map.Entry<String,String> e = it.next();if (needRemove(e)) it.remove();
    }
  2. 不要把顺序当成 equals
    LinkedHashMap.equals 只比较内容与顺序无关,两个顺序不同的 map 仍可能相等。

  3. 线程安全
    所有方案均非同步,并发场景请用 Collections.synchronizedMap 包一层,或使用 ConcurrentLinkedHashMap(Guava)。


6. 结论

  • accessOrder=true 一旦开启,get 必定移动节点,无法通过参数关闭

  • 真正“只读不动序”只有两条路:
    ① 根本不调用 get,用迭代器;
    ② 调用但不走 get,用反射或快照。

  • 根据数据规模、性能、代码维护成本三选一即可;反射方案在缓存中间件、LRU 统计等高频只读场景下性价比最高。

7.补充方案

    public static void main(String[] args) {Map<String, String> map = new LinkedHashMap<>(16, 0.75f, true);map.put("A", "1");map.put("B", "2");ArrayList<String> strings = new ArrayList<>(map.keySet());System.out.println("ceshi" + strings.get(0));System.out.println(map.keySet()); // 输出 [B, A]}

http://www.dtcms.com/a/394312.html

相关文章:

  • 破解K个最近点问题的深度思考与通用解法
  • 链式结构的特性
  • 报表1-创建sql函数get_children_all
  • 9月20日 周六 农历七月廿九 哪些属相需要谨慎与调整?
  • godot实现tileMap地图
  • 【Unity+VSCode】NuGet包导入
  • QEMU虚拟机设置网卡模式为桥接,用xshell远程连接
  • Week 17: 深度学习补遗:Boosting和量子逻辑门
  • 【论文速递】2025年第13周(Mar-23-29)(Robotics/Embodied AI/LLM)
  • Webpack进阶配置
  • 【LeetCode 每日一题】3227. 字符串元音游戏
  • 【图像算法 - 26】使用 YOLOv12 实现路面坑洞智能识别:构建更安全的智慧交通系统
  • 009 Rust函数
  • IT疑难杂症诊疗室
  • 视频播放器下载推荐,PotPlayer‌,KMPlayer,MPC-HC,GOM Player‌VLC media player,MPV,
  • Day04 分治 递归 | 50. Pow(x, n)、22. 括号生成
  • (博主大回归)洛谷题目:P1986 元旦晚会 题解 (本题简)
  • Windows Docker 环境下 VLLM 大模型存储最优解:Docker-Desktop 实例目录与多容器协同挂载方案
  • Elasticsearch面试精讲 Day 20:集群监控与性能评估
  • 如何解决 pip install 安装报错 ModuleNotFoundError: No module named ‘pydantic’ 问题
  • 设置永不待机 系统语言
  • PWA(渐进式Web应用)
  • gdb文档_第二章
  • 基础IO
  • Linux开发工具
  • DIDCTF-2023陇剑杯
  • 软件设计师软考备战:第四篇 计算机网络技术
  • 基于 GEE 利用 Sentinel-1 SAR 数据计算标准化双极化水体指数(SDWI)实现水体智能识别
  • 120-armv8_a_power_management:高级架构电源管理指南
  • 【MySQL初阶】02-库的操作