第032篇 LinkedHashMap 与 LRU:图片缓存的原型

简介: Android面试高阶题常以LinkedHashMap与LRU为切入点,考察“顺序”这一关键维度:它通过双向链表维护插入/访问序(accessOrder=true时get/put自动移至尾部),配合removeEldestEntry可几行实现线程不安全但高效的LRU缓存。LruCache即基于此构建。

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

有任何问题欢迎在评论区留言交流。

相关文章
|
2天前
|
人工智能 安全 数据挖掘
数据分析工具推荐与选型指南:如何找到最适合业务的解决方案
本文为数据分析工具选型指南,强调“先诊断、再选型”:聚焦“谁在用、数据在哪、是否闭环”三大前提,从数据接入、指标治理、AI自助分析、安全管控、系统集成五大维度评估。以阿里云Quick BI为例,详解其50+数据源直连、语义建模、智能小Q自然语言分析、行列级权限及办公平台联动等能力,助力企业高效落地数据价值。
|
2天前
|
消息中间件 Java 调度
第038篇 Thread 与 Runnable:线程生命周期全解
Thread与Runnable本质是“任务”与“执行单元”的分离:Runnable仅定义要做的事(无返回、不抛检异常),Thread负责调度执行(含状态、优先级、生命周期控制)。`start()`才真正启新线程,`run()`只是普通方法调用。常见坑包括误调`run`导致伪并发、异常静默终止、持有Activity引发内存泄漏。工程中应优先使用线程池而非裸Thread。
20 0
|
1天前
|
监控 Java Android开发
第045篇 JVM 内存区域:堆、栈、方法区与程序计数器
JVM内存区域是面试分水岭:非死记硬背,而要理解线程私有(栈、PC计数器、本地方法栈)与共享(堆、元空间)的本质差异,能据OOM类型精准定位根因——栈溢出看调用链、堆OOM析对象引用、元空间OOM查类加载、直接内存OOM盯NIO缓冲。
19 0
|
1天前
|
JavaScript 算法 Java
第120篇 tailrec 与尾递归:递归优化的编译期支持
`tailrec` 是 Kotlin 的编译期优化机制:当函数满足尾调用条件时,编译器将其重写为循环,避免栈溢出。它不依赖 JVM(JVM 无 TCO),而是 Kotlin 自主实现;不支持 `suspend`、非尾位置调用或需回溯的场景。本质是“递归定义 → 累加器循环”的转换。
21 2
|
1天前
|
Android开发
第122篇 Activity 启动模式:standard 到 singleInstance
本节详解Activity启动模式,直击“最容易出玄学bug”的栈管理机制。核心指出:行为由**系统、任务栈、launchMode、Intent flags四者共同决定**,仅记四个枚举值远远不够。重点剖析四种任务栈本质及flags优先级,并给出通知跳转、登录回退、启动图等真实场景的避坑方案与代码范式。
15 1
|
1天前
|
Java 定位技术 开发工具
第059篇 建造者模式:链式调用为何无处不用在哪些地方
建造者模式核心是**将复杂对象的创建过程外置、分步可控、集中校验**。它解决参数过多、互斥依赖、分步初始化问题,关键在于:私有构造、链式setter、build()统一校验、产物不可变(final+防御拷贝)、默认值内聚于Builder——非仅为语法糖,而是工程化构造控制。
27 1
|
1天前
|
JSON Java API
第082篇 reified 泛型:擦除限制的破局者
`reified` 依赖 `inline` 的根本原因是 JVM 泛型擦除——运行期无法获取 `T` 的类型信息。`reified` 并非“恢复”泛型,而是在编译期借助内联,将调用点的实际类型(如 `String`)直接代入函数体,使 `x is T`、`T::class.java` 等操作合法。它仅适用于编译期已知类型,不支持运行时动态类型,且会增加代码体积。工程中应优先使用 AndroidX 官方显式类型 API,必要时提供 `Class&lt;T&gt;` 重载以兼顾灵活性与兼容性。
11 0
|
1天前
|
传感器 Java 测试技术
第095篇 Flow 操作符进阶:debounce 与 combine
本节深入剖析 Flow 常用操作符的工程本质,聚焦面试高频痛点:为何“代码能跑却线上出错”。按转换、组合、副作用、控制四类梳理,并透彻讲解 `flatMapLatest/merge/concat` 区别、`catch` 作用域、`flowOn` 影响范围等关键原则,辅以搜索防抖、轮询重试等真实落地范式。
20 0
|
1天前
|
Java 测试技术 API
第108篇 Channel 与 select:协程间通信
本节详解 Kotlin 协程核心通信原语 Channel:它是点对点消息通道,比 Flow 更底层、比回调更结构化。涵盖四种容量策略、`onBufferOverflow` 行为、`select` 多路等待机制,并厘清 Channel 与 Flow 的本质区别——事件驱动选 Channel,数据流处理选 Flow。
21 0
|
1天前
|
缓存 API Android开发
第125篇Fragment 通信演进:从接口回调到共享 ViewModel
本文梳理Fragment通信演进史,揭示五代方案(接口回调、setFragmentResult、广播、共享ViewModel、Fragment Result API)的耦合本质。核心结论:**仅推荐三种场景化方案——Fragment Result API传一次性结果、共享ViewModel管共同状态、事件总线限于跨模块解耦**。重点剖析“耦合藏在哪”,并指出`viewLifecycleOwner`误用、事件重复消费、ViewModel状态污染等高频坑及自动化防治策略。
19 0

热门文章

最新文章