1. 概述
Java 8 为 Map
接口引入了一个非常实用的默认方法:computeIfAbsent
。这个方法在处理“缓存”、“懒加载”、“避免重复计算”等场景时极为高效,能帮你写出更简洁、线程安全的代码。
本文将深入解析该方法的签名、典型用法以及各种边界情况的处理方式。✅ 掌握它,能让你在集合操作中少写很多 if-else 踩坑。
2. Map.computeIfAbsent 方法详解
先看方法签名:
default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
参数说明:
key
:要查询的键mappingFunction
:一个函数式接口,用于在键不存在时计算值(key -> value
)
⚠️ 关键点:**mappingFunction
只有在 key 不存在或对应 value 为 null 时才会执行**。这是它比手动 if(map.get(key) == null)
更高效、更安全的核心原因。
2.1 键已存在且值非 null
如果 key
已存在,并且对应的 value
不是 null
,则直接返回现有值,不会执行 mappingFunction。
Map<String, Integer> stringLength = new HashMap<>();
stringLength.put("John", 5);
assertEquals((long) stringLength.computeIfAbsent("John", s -> s.length()), 5);
✅ 注意:虽然 "John".length()
是 4,但由于 map 中已有值 5,所以 computeIfAbsent
直接返回 5,lambda 表达式根本没执行。这是线程安全的关键。
2.2 键不存在或值为 null,执行 mappingFunction
如果 key
不存在,或其值为 null
,则会调用 mappingFunction
计算新值,并将结果放入 map(除非计算结果为 null
)。
Map<String, Integer> stringLength = new HashMap<>();
// key 不存在,执行 s -> s.length(),返回 4
assertEquals((long) stringLength.computeIfAbsent("John", s -> s.length()), 4);
// 值已被缓存
assertEquals((long) stringLength.get("John"), 4);
✅ 这个特性非常适合做“懒加载”或“缓存初始化”,比如:
// 缓存用户权限,避免重复查询数据库
Map<String, List<String>> userPermissions = new ConcurrentHashMap<>();
List<String> permissions = userPermissions.computeIfAbsent(userId, this::loadPermissionsFromDB);
2.3 mappingFunction 返回 null
如果 mappingFunction
显式返回 null
,那么 不会 将 null
存入 map,map 保持原状(即该 key 仍不存在)。
Map<String, Integer> stringLength = new HashMap<>();
// 函数返回 null,map 不会记录任何映射
assertEquals(stringLength.computeIfAbsent("John", s -> null), null);
// 确认 key 不存在
assertNull(stringLength.get("John"));
⚠️ 注意:这与 put(key, null)
不同,后者会显式存入 null
。computeIfAbsent
的设计避免了缓存“空值”导致的误判。
2.4 mappingFunction 抛出异常
如果 mappingFunction
抛出未检查异常(unchecked exception),该异常会直接向上抛出,不会 修改 map。
@Test(expected = RuntimeException.class)
public void whenMappingFunctionThrowsException_thenExceptionIsRethrown() {
Map<String, Integer> stringLength = new HashMap<>();
stringLength.computeIfAbsent("John", s -> {
throw new RuntimeException("计算失败");
});
}
✅ 这意味着你需要在 mappingFunction
内部处理可能的异常,否则会中断流程。简单粗暴但有效。
3. 总结
computeIfAbsent
是 Java 8 集合 API 的一大亮点,它的原子性操作让你无需额外同步就能安全地实现:
- ✅ 缓存加载(如:ConcurrentHashMap + computeIfAbsent)
- ✅ 避免重复计算
- ✅ 构建嵌套集合(如
Map<String, List<T>>
)
只要记住它的核心逻辑:只在 absence 时 compute,且 null 不缓存,就能避免大多数踩坑。
所有示例代码均可在 GitHub 获取:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-collections-maps-3