LRU Cache事件监听器应遵循PECS原则:淘汰事件中被驱逐项为生产者,参数用? extends K/V;写入回调中监听器为消费者,参数用? super K/V。

在 Java 中,LRU Cache 的事件监听器(如 CacheWriter 或自定义淘汰回调)若需泛型化设计,PECS(Producer Extends, Consumer Super)原则能显著提升类型安全性与复用性——关键在于:**被驱逐的元素是“生产者”,监听器参数应使用 ? extends K/V;而监听器若需接收新值或执行写入操作,则输入参数应为 ? super K/V。**
淘汰事件中的“被驱逐项”是生产者 → 用 ? extends
当 LRU Cache 触发淘汰并通知监听器时,它向外提供(produce)一个已存在的键或值(比如 onEviction(K key, V value))。此时监听器只读取、不修改该对象,属于“数据生产者”。按 PECS,泛型参数应声明为上界通配符:
-
✅ 正确示例:
void onEviction(? extends K key, ? extends V value)—— 允许监听器接收任何K的子类型实例(如String缓存可被Object类型监听器监听) -
❌ 错误倾向: 写成
? super K会导致无法安全读取具体字段(编译器只允许调用Object方法),失去业务意义
写入/更新回调中监听器是消费者 → 用 ? super
若监听器承担写后处理职责(例如将淘汰项同步到数据库、日志系统),它需要“消费”(consume)键值对。此时应支持接收原始类型及其父类型,便于复用:
-
✅ 正确示例:
interface CacheWriter super K, ? super V> { void write(K key, V value); }—— 一个接受Serializable的写入器可同时处理String和Integer缓存 - 实际中常见于 Guava 的
RemovalListener设计:其onRemoval(RemovalNotification extends K, ? extends V>)对通知对象用extends(通知是产出),但内部getKey()/getValue()返回具体类型,供监听器安全使用
避免混用:监听器接口自身泛型 ≠ 事件参数泛型
不要把监听器类的类型参数直接套用到回调方法上。例如:
-
易错写法:
class LoggingListener+implements RemovalListener public void onRemoval(RemovalNotification—— 强制监听器与缓存类型完全一致,丧失灵活性n) -
推荐写法:
interface RemovalListener定义为void onRemoval(RemovalNotification extends K, ? extends V> notification)—— 接口保持协变,使用者可传入更宽泛的监听器实现
实战建议:结合 LRU 实现选型判断
不同 LRU 实现对泛型支持程度不同,设计监听器前先确认底层行为:
- Guava Cache:原生支持
RemovalListener super K, ? super V>(注意:Guava 实际用的是RemovalListener,但其RemovalNotification内部字段为? extends K/V,符合 PECS) - Caffeine:
RemovalListener回调参数为RemovalCause和原始K/V,但推荐封装一层适配器,对外暴露onEvict(? extends K, ? extends V)以兼容多态 - 手写 LRU(如 LinkedHashMap 子类):在
removeEldestEntry或自定义回调中,显式用extends K>声明监听器函数式接口参数,避免类型擦除隐患
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











