computeIfAbsent用法讲解
这是Java 中 Map 接口的 computeIfAbsent
方法。这是一个非常强大且实用的方法,可以极大地简化代码。
方法定义
首先,我们来看一下它的方法签名:
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
- key: 要与指定值关联的键。
- mappingFunction: 一个函数式接口(通常是 Lambda 表达式或方法引用)。它接受 key 作为输入,并计算(Compute)出要返回的值。只有当 key 对应的映射不存在(Absent)或为 null 时,这个函数才会被调用。
- 返回值: 返回与指定键关联的当前(现有的或新计算的)值;如果计算后的值仍为 null,则返回 null。
核心思想
computeIfAbsent
的行为可以概括为一句话::“如果不存在,则计算并放入”
检查 Map 中是否存在这个 key。如果不存在(或者对应的 value 是 null),则使用 mappingFunction 计算出一个新值,将这个 key-value 对放入 Map,并返回这个新值。如果 key 已经存在,则直接返回已存在的 value,mappingFunction 不会被调用。
它的内部逻辑类似于以下代码:
if (map.get(key) == null) {V newValue = mappingFunction.apply(key); // 根据key计算新值if (newValue != null) {map.put(key, newValue); // 如果新值不为空,则放入Mapreturn newValue;}
}
return map.get(key); // 如果key已存在或新值为空,返回现有的值(可能为null)
优点
在没有 computeIfAbsent
之前,我们通常这样写代码:
Map<String, List<String>> map = new HashMap<>();
String key = "fruits";// 传统写法
List<String> list = map.get(key);
if (list == null) {list = new ArrayList<>();list.add("Apple");map.put(key, list);
} else {list.add("Apple");
}// 使用 computeIfAbsent
List<String> list = map.computeIfAbsent(key, k -> new ArrayList<>());
list.add("Apple");
优点对比:
简洁性: 将检查、计算、放入和返回多个步骤合并成了一个原子操作,代码更加简洁。
原子性: 在多线程环境下,如果使用的是 ConcurrentHashMap
,computeIfAbsent 的执行是线程安全的。整个“检查-计算-放入”过程是一个同步块,避免了竞态条件。
表达清晰: 代码的意图非常明确——“如果这个 key 没有值,就创建一个新的列表给我”。
使用场景
场景 1:分组归类(最常见)
这是一个非常经典的用法,用于构建一个“每个键对应一个集合”的 Map。
Map<String, List<Student>> studentsByClass = new HashMap<>();for (Student student : allStudents) {// 如果 className 不存在,就创建一个新 ArrayList 并放入Map// 然后返回这个 List(无论是新创建的还是已存在的),接着添加 studentstudentsByClass.computeIfAbsent(student.getClassName(), k -> new ArrayList<>()).add(student);
}
场景 2:缓存(Lazy Loading)
实现一个简单的缓存,只有在需要时才计算值。
Map<String, BigDecimal> priceCache = new HashMap<>();public BigDecimal getPrice(String productId) {return priceCache.computeIfAbsent(productId, this::calculatePriceFromDatabase);
}// 这是一个耗时的方法,只在第一次获取某个 productId 的价格时调用
private BigDecimal calculatePriceFromDatabase(String productId) {// ... 模拟复杂的数据库查询或网络请求return BigDecimal.TEN;
}
场景 3:初始化复杂对象
确保 Map 中的每个键都有一个初始化好的、非空的复杂对象。
Map<Integer, Config> configMap = new HashMap<>();// 获取 ID 为 123 的配置,如果没有则用默认配置初始化
Config config = configMap.computeIfAbsent(123, id -> new Config(...));
config.doSomething();
注意事项
mappingFunction 不应返回 null: 如果 mapping function 计算后返回 null,则不会创建任何映射,该方法也会返回 null。这通常不是你想要的,可能会导致后续的 NullPointerException。
ConcurrentHashMap 的线程安全性: 在 ConcurrentHashMap 中,computeIfAbsent 是线程安全的。整个计算过程是在锁内执行的,这意味着 mappingFunction 不应该是一个耗时很长的任务,否则会成为性能瓶颈。
递归调用: 严禁在 ConcurrentHashMap 的 computeIfAbsent 的 mappingFunction 中尝试再次修改这个 Map 本身(例如,再次调用 computeIfAbsent 或 put),这可能会导致死锁。