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) 不同,后者会显式存入 nullcomputeIfAbsent 的设计避免了缓存“空值”导致的误判。

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


原始标题:The Map.computeIfAbsent() Method