
日常开发中只要提到键值对存储大家闭着眼睛都会new HashMap()。毕竟它的查找和插入速度几乎无敌时间复杂度逼近 O(1)。但HashMap有个致命的“薛定谔的顺序”问题你怎么存进去的和它怎么遍历出来的完全是两码事。如果业务场景要求“怎么存进去的就怎么取出来”比如渲染有序菜单、记录任务先后顺序该怎么办今天我们要请出 Java 集合框架里的隐藏大佬——LinkedHashMap。用一句话概括它它就是一个能记住元素顺序的 HashMap。核心机制它是如何记住顺序的是什么很多初学者觉得LinkedHashMap很高深其实它的底层公式极其简单核心公式LinkedHashMap HashMap 双向链表场景化降维想象一个巨大的露天停车场HashMap的哈希桶数组。为了最高效利用空间管理员让每辆进来的车根据车牌号算出一个数字直接停到对应的车位上。这导致车辆在整个停车场里是完全散乱分布的你根本不知道哪辆车是先来的哪辆是后来的。这时候换了一个聪明的管理员LinkedHashMap。他依然让车子随便按哈希值停但他手里拿了一根红色的长绳子。 第一辆车进来绳子的一头系在第一辆车上第二辆进来绳子从第一辆车拉过去系在第二辆车上……以此类推。 最后无论这些车在停车场里停得多么天女散花只要你顺着这根红绳子走双向链表就能完美还原所有车辆入场的绝对顺序两大杀手锏与极简实战怎么用LinkedHashMap不仅能记住“插入顺序”它还隐藏了另一个大招“访问顺序”。1. 插入顺序默认模式先进先出默认情况下它就像排队买喜茶先来的排前面后来的排后面。2. 访问顺序Access Order这是LinkedHashMap的灵魂特性。当你在构造函数中将accessOrder参数设置为true时它的排序规则就变了。场景化降维就像你手机后台的“最近打开应用列表”。你刚才打开了微信微信就排在最前面此时你切出去刷了一下抖音抖音就被顶到了最前面而太久没点开的 App 则会沉底甚至被系统杀后台淘汰掉。 只要你对某个元素执行了get()或put()操作这个元素就会被立刻“摘”下来重新挂到双向链表的尾部。来看看极简的代码实战Javimport java.util.LinkedHashMap; import java.util.Map; public class LinkedHashMapDemo { public static void main(String[] args) { // 划重点第三个参数 true 代表开启“访问顺序”模式 // 初始容量 16负载因子 0.75f开启访问顺序 LinkedHashMapString, String appCache new LinkedHashMap(16, 0.75f, true); appCache.put(App1, 微信); appCache.put(App2, 抖音); appCache.put(App3, 淘宝); System.out.println(初始顺序); System.out.println(appCache.keySet()); // 模拟用户刚刚打开了“微信” appCache.get(App1); System.out.println(访问微信后的顺序); System.out.println(appCache.keySet()); } } /* 运行输出结果 初始顺序 [App1, App2, App3] 访问微信后的顺序 [App2, App3, App1] -- 重点被访问的微信(App1)被移动到了链表最尾部 */高光时刻几行代码手撕 LRU 缓存重点细节一道常见的代码题“请设计一个 LRU最近最少使用缓存机制”。 如果你从头手写双向链表加 HashMap很容易在指针的断开与重连中搞出 NullPointerException。但有了LinkedHashMap你只需要几行代码就能完成绝杀。JDK 老司机在LinkedHashMap内部预留了一个优雅的钩子方法removeEldestEntry()。每次执行put()之后系统都会调用这个方法问你要不要删掉最老的那个元素也就是链表头部的元素。默认它返回false你只需要重写它即可。Javaimport java.util.LinkedHashMap; import java.util.Map; // 继承 LinkedHashMap泛型指定 K 和 V public class SimpleLRUCacheK, V extends LinkedHashMapK, V { private final int capacity; // 缓存最大容量 public SimpleLRUCache(int capacity) { // 容量设置负载因子默认开启访问顺序 true super(capacity, 0.75f, true); this.capacity capacity; } // 重写钩子方法当当前容量超过设定的最大容量时返回 true触发淘汰机制 Override protected boolean removeEldestEntry(Map.EntryK, V eldest) { return size() capacity; } public static void main(String[] args) { SimpleLRUCacheInteger, String lru new SimpleLRUCache(3); lru.put(1, 数据A); lru.put(2, 数据B); lru.put(3, 数据C); System.out.println(填满3个后: lru.keySet()); // [1, 2, 3] lru.get(1); // 访问数据A数据A移动到尾部 lru.put(4, 数据D); // 插入新数据容量超载淘汰最久未使用的头部数据此时是2 System.out.println(访问1并插入4后: lru.keySet()); } } /* 运行输出结果 填满3个后: [1, 2, 3] 访问1并插入4后: [3, 1, 4] -- 重点最久未被访问的2被淘汰最近访问的1和新插入的4都在后面 */源码浅析JDK 老司机的优雅设计知其然更知其所以然LinkedHashMap为什么能做到这么无缝的切换其实它本质上就是稍微魔改了一点点HashMap的底层节点。普通的HashMap节点Node只有next指针用来解决哈希冲突的单链表。 而LinkedHashMap搞了个静态内部类Entry继承了HashMap.Node并强行塞入了两个新指针Java// LinkedHashMap.Entry 的极简示意 static class EntryK,V extends HashMap.NodeK,V { EntryK,V before, after; // 就是这俩兄弟串起了双向链表 Entry(int hash, K key, V value, NodeK,V next) { super(hash, key, value, next); } }底层逻辑闭环在HashMap的源码里如果你眼尖会发现有几个空方法afterNodeAccess()、afterNodeInsertion()。 这在设计模式里叫模板方法模式。HashMap把骨架搭好留几个空方法。LinkedHashMap则重写了这几个方法。每次get()完触发afterNodeAccess()把节点剪切到链表尾部。每次put()完触发afterNodeInsertion()调用removeEldestEntry()决定要不要干掉链表头。避坑指南与高频面试对比1. 性能陷阱虽然增删改查依然维持在 O(1)但天下没有免费的午餐。因为每个节点多了before和after两个引用指针LinkedHashMap的内存消耗比普通的 HashMap 要大。而且每次操作都需要额外维护双向链表的指针变动极端高并发或极致性能要求的场景下插入速度会略微受影响。2. 并发噩梦它是非线程安全的千万不要在多线程环境下直接拿上面的SimpleLRUCache来用。如果有并发需求请使用Collections.synchronizedMap包装一下或者在生产环境直接拥抱工业级的本地缓存框架Guava Cache 或 Caffeine基于 W-TinyLFU吊打传统 LRU。