覆写 removeEldestEntry 是实现容量限制 LinkedHashMap 的唯一可靠方式,必须配合 accessOrder = true(LRU)或 false(FIFO)构造器使用,且不可手动删 entry 破坏机制。

重写 removeEldestEntry 是唯一可靠方式
Java 标准库不提供开箱即用的“容量限制 Map”,LinkedHashMap 是唯一内置支持该能力的实现,关键就在覆写 removeEldestEntry 方法。它不是钩子函数、不是配置项,而是每次 put 或 putAll 后被调用的判断逻辑 —— 返回 true 就删最老条目(按访问顺序或插入顺序),否则不删。
常见错误是只重写但没调用父类构造器指定 accessOrder = true,导致行为不符合缓存预期;或者在方法里加了复杂逻辑(比如查数据库),拖慢写入性能。
-
removeEldestEntry在put完成后立即触发,不等下次操作 - 必须配合
new LinkedHashMap(initialCapacity, loadFactor, accessOrder)构造,accessOrder = true才能按最近访问排序(LRU) - 返回
true时删除的是当前最老 entry,不是“将要插入”的那个 - 如果 map 还没满,该方法仍会被调用,只是你通常返回
false
accessOrder = false 和 true 的行为差异直接影响缓存语义
默认 accessOrder = false(插入顺序),removeEldestEntry 删除的是最早插入的条目 —— 类似 FIFO 队列;设为 true 后,每次 get 或 put 都会把对应 entry 移到链表尾,最老的就是长期未访问的 —— 这才是 LRU 缓存的核心。
容易踩的坑:误以为 get 不触发重排,其实只要 accessOrder = true,get 就会更新位置;另外,containsKey 和 getOrDefault 不触发重排,只有 get 和 put 会。
立即学习“Java免费学习笔记(深入)”;
- FIFO 场景用
accessOrder = false,适合日志缓冲、消息队列等不关心访问热度的场景 - LRU 缓存必须用
accessOrder = true,且注意get是“访问”,会改变顺序 - 构造时传错
accessOrder值,会导致缓存淘汰完全偏离预期,现象是“刚 get 过的 key 很快就被删了”
容量控制不能依赖 size() 判断 + 手动 remove
有人试图在每次 put 后检查 map.size() > maxSize,再手动删第一个 entry。这不仅破坏 LinkedHashMap 内部链表一致性(可能引发 ConcurrentModificationException),还会让 removeEldestEntry 失效 —— 因为它只在 put 流程中被框架调用一次,手动删绕过了整个机制。
更隐蔽的问题是:多线程下手动删 + size 判断存在竞态,两个线程同时发现 size 超限,都去删,结果删多一个;或者一个线程刚删完,另一个线程还没来得及 check,又 put 进去,瞬间超限。
- 永远不要在外部调用
remove来维持容量,交给removeEldestEntry统一处理 -
size()是 O(1),但频繁检查 + 手动删会让代码变脆,且无法与accessOrder协同工作 - 如果真需要自定义淘汰策略(比如按 value 大小删),应继承
LinkedHashMap并在removeEldestEntry里实现,而不是绕开它
注意 put 重复 key 时不触发淘汰,putIfAbsent 也不触发
这是最容易被忽略的边界:当 put(key, value) 的 key 已存在,LinkedHashMap 只更新 value、移动 entry 位置(若 accessOrder = true),但不会调用 removeEldestEntry —— 因为 size 没变。同样,putIfAbsent 成功才调用,失败则什么也不做。
这意味着:如果你的缓存大量更新已有 key(比如计数器、状态快照),map size 会长期卡在上限,新 key 进不来,但旧 key 也不会被淘汰 —— 表现为“缓存僵死”,看似满了却不再换血。
- 确认业务是否允许 key 更新,如果高频更新是常态,需评估是否真需要固定大小,或改用带过期时间的方案(如 Caffeine)
- 测试时别只测新增,一定要覆盖
put同 key 多次的 case - 没有银弹:LRU + 固定大小只适合“读多写少、key 稳定增长”的缓存场景










