Android 面试走到高阶环节,考察方式会从“问知识点”切换成“沿着知识点连续追问”。LinkedHashMap 与 LRU 就是常被选中的切入口:一层层问下去,知识的边界在哪里,候选人自己最清楚。这题考的不是背定义,而是能不能把“顺序”这个常被忽略的维度讲出用处。
先把结论放在前面:LinkedHashMap 在 HashMap 的桶结构之外,额外用一条双向链表维护“插入顺序”或“访问顺序”(由 accessOrder 决定)。开启访问序后,get / put 都会把节点移到链表尾部,于是头部是那些“很久没被碰过”的元素;配合重写 removeEldestEntry,就能用几行代码实现一个固定容量的 LRU 缓存。 这题要答好,得把“顺序从哪来、怎么被改变、怎么用来淘汰”讲透。
机制拆解
讲清顺序的维护方式。HashMap 的节点本身只管哈希桶,LinkedHashMap 的节点多了 before / after 两个指针,串成一条贯穿所有条目的链表;accessOrder 为 false 时按插入先后排,为 true 时每次访问都重排。get 在 accessOrder=true 下会调用 afterNodeAccess 把当前节点移到尾端;put 成功或覆盖后也会根据情况调整。 删除最旧节点由 removeEldestEntry 控制,默认返回 false(不淘汰),子类重写它就能在容量超阈值时剔除头部。
这些坑的正确绕法
最常见的坑是忘记开启 accessOrder。很多人直接 new LinkedHashMap 就拿来做缓存,结果维护的是插入序而非访问序,热点数据被早早踢出、冷数据反而留下,命中率上不去。要做 LRU 需要显式传 accessOrder=true。
其次是重写 removeEldestEntry 时只判断 size 却忘了它不保证线程安全。LinkedHashMap 本身是 HashMap 家族,非并发安全;在多线程环境直接当共享缓存用,会出现结构损坏或数据错乱。并发缓存该用 ConcurrentHashMap 配合外部结构,或专门的缓存库。
还有一个更隐蔽的坑:以为 LinkedHashMap 的迭代顺序“随便看看就行”。正因为它的顺序可预测(插入序或访问序),很多代码会依赖这个顺序做稳定输出或单元测试断言;一旦有人把 accessOrder 改错,依赖方会悄悄出错且难以定位。顺序是一种契约,改动要谨慎。
代码里见真章
看一段能直接跑的代码,把上面的机制落到具体写法上:
// LinkedHashMap 实现 LRU 缓存
LinkedHashMap<String, Integer> cache =
new LinkedHashMap<>(16, 0.75f, true) {
// accessOrder = true
@Override
protected boolean removeEldestEntry(Map.Entry<String, Integer> e) {
return size() > 3; // 容量超过 3 即淘汰最早进入的元素
}
};
cache.put("a", 1);
cache.get("a"); // 访问序下,get 也会把节点移到尾部
// 图片缓存的经典底座:容量上限 + 淘汰策略 + 可预测的访问序
这段代码值得盯三处:第一处,构造器第三个参数 accessOrder=true 是开启 LRU 的总开关;第二处,removeEldestEntry 这个钩子让子类用一行判断就实现淘汰;第三处,get 也会调整顺序,这正是“最近使用”语义的来源。面试讲到这一层,基本就稳了。
这题在面试里怎么问、怎么答
“请简单介绍一下 LinkedHashMap 与 LRU,它在 Android 开发中起什么作用?”一句话:在哈希表上叠加可预测的顺序,实现 LRU 缓存。在 Android 里,LruCache 内部正是用 LinkedHashMap 承载 Bitmap 等资源的回收决策;理解它能讲清内存缓存为什么“老图先走”。 如果要把约定沉淀成团队规范,建议加一条:用 LinkedHashMap 做缓存需要显式开启 accessOrder 并审阅 removeEldestEntry 的阈值。
“LinkedHashMap 的底层原理是什么?能不能详细说一下?”从双链表的 before/after 指针讲起,对比 HashMap 仅维护桶;再讲 accessOrder 如何改变 get/put 的重排行为,最后落到 removeEldestEntry 的淘汰钩子。机制别空讲,配核心代码最稳。
“在使用时遇到过什么问题?”客观地说实案例:某内存缓存命中率长期偏低,排查发现 LinkedHashMap 误用插入序,高频资源被新写入顶掉;改为 accessOrder=true 后命中率回升、OOM 次数下降。量化的修复效果最加分。
“和相关的替代方案相比,有什么优劣?”与 HashMap 比,LinkedHashMap 多了顺序保障、略有指针开销;与 TreeMap 比,LinkedHashMap 按访问而非比较排序,前者适合缓存淘汰、后者适合有序遍历;与专用缓存库(如带过期、统计的)比,LinkedHashMap 轻量但功能薄。选型时要把“非线程安全、需手动管阈值”这类代价摆到台面上,再决定是否引入。
再补一个工程上值得讲清的点:LinkedHashMap 与 Android LruCache 的关系。LruCache 并没有自己实现链表,而是内部持有一个 accessOrder=true 的 LinkedHashMap,把“被使用”的逻辑委托给它的顺序语义,自身只负责 size 计量、evict 回调与线程安全(LruCache 用 synchronized 包住操作)。 所以看懂 LinkedHashMap 的访问序,基本就看懂了 LruCache 一半的核心。另一个常被追问的是:为什么 LRU 选 LinkedHashMap 而不是自己维护队列?因为哈希查找 O(1) 与顺序维护 O(1) 同时需要,LinkedHashMap 把两者合二为一,比“HashMap + 双端队列”两套结构手动同步更不易出错。理解了这点,就能解释它在缓存场景里为什么是默认底座。
还有一个容易忽略的点是 LRU 的“近”与“旧”。所谓 LRU 是“近期未被使用就淘汰”,可真实业务里“最近没访问”未必等于“价值最低”——一个用户偶尔看一眼的冷门详情页,可能比一个被后台轮询反复 touch 的统计接口更值得留在缓存里。 Android 框架的 LruCache 正是用 sizeOf 按字节计量,把 Bitmap 这类大对象的淘汰纳入决策,这一点面试里点出来,说明没把 LRU 当成教条。顺带把三种淘汰口径也分清:LRU 淘汰近期未用的,FIFO 淘汰先进先出的,LFU 淘汰使用频次低的;LinkedHashMap 只天然支持 LRU,另两种需要自己维护计数或队列。 这也是面试官追问“LFU 怎么用 LinkedHashMap 实现”时真正的考点——答案是在 value 里挂访问次数,落盘时机要选 put 之后、get 之后各做一次自增与重排。
给正在准备面试的你
把 LinkedHashMap 画成“HashMap 桶 + 一条贯穿的双向链表”,标出 accessOrder 开关与 removeEldestEntry 钩子。面试中关于 LinkedHashMap 与 LRU 的问题,关键在于能够从原理、应用、踩坑三个层面给出有深度的回答。
再补工程案例与踩坑——应用落点是用 LinkedHashMap 实现一个 LRU 缓存,对比它与 Android LruCache 源码中的同步与容量处理。
复习时别孤立刷题:TreeMap 与排序——相邻考点常被一起问,边界提前划清楚。
划两句重点:accessOrder=true 让 get/put 都重排访问序,removeEldestEntry 一行实现淘汰;它非线程安全,Android LruCache 用它在内部承载回收决策。
下一篇聊 TreeMap 与排序:Comparable 与 Comparator——沿着今天这条主线继续往前走。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:ConcurrentHashMap:从分段锁到-CAS-加-synchronized
下一篇预告:TreeMap-与排序:Comparable-与-Comparator
有任何问题欢迎在评论区留言交流。