computeIfAbsent 是 Java Map 接口中的一个方法,用于简化“如果键不存在则初始化值”的操作。以下是该方法的详细解释及示例代码的分析:
方法语法
// 方法签名
default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
• 功能:
如果 key 不存在于 Map 中,则通过 mappingFunction 计算新值并插入 Map;如果 key 已存在,则直接返回当前值。
• 参数:
• key:要查找或计算的键。
• mappingFunction:一个函数,接受 key 作为输入,返回要插入的新值(仅在键不存在时调用)。
• 返回值:与 key 关联的当前值(已存在或新计算的)。
代码解析
cache.computeIfAbsent(c, k -> new ArrayList<>()).add(new int[]{i, j});
步骤拆解
-
检查键是否存在:
检查 Mapcache中是否存在键c(字符类型)。 -
处理键不存在的情况:
• 如果c不存在,调用mappingFunction(即k -> new ArrayList<>()),创建一个新的空ArrayList。 • 将新列表作为值插入 Map,键为c。 • 返回这个新列表。 -
处理键存在的情况:
• 直接返回键c对应的现有列表。 -
添加坐标到列表:
无论键是否存在,最终都会获得一个列表(可能是新创建的或已存在的),然后将坐标[i, j](以int[]形式)添加到这个列表中。
关键点
• 避免重复检查:
传统写法需要先检查 if (!cache.containsKey(c)),再手动插入新列表,而 computeIfAbsent 将两步合并,代码更简洁。
• Lambda 表达式的作用:
k -> new ArrayList<>() 是一个函数,接受键 k(即 c)作为输入,但在此场景中未实际使用 k,因为无论键是什么,都返回一个新列表。
• 链式操作:
方法返回列表后,直接调用 add 方法插入坐标,体现了函数式编程的链式调用风格。
与传统写法的对比
// 传统写法
List<int[]> list = cache.get(c);
if (list == null) {
list = new ArrayList<>();
cache.put(c, list);
}
list.add(new int[]{i, j});
// computeIfAbsent 写法(一行替代)
cache.computeIfAbsent(c, k -> new ArrayList<>()).add(new int[]{i, j});
• 优势:减少代码行数,避免显式的 null 检查,提高可读性。
适用场景
• 缓存初始化:如上述代码中的字符位置缓存。 • 按需创建资源:例如数据库连接池、线程池等需要懒初始化的场景。 • 统计分组:按某个键动态聚合数据。
注意事项
• 线程安全:computeIfAbsent 本身不是原子操作,如果在多线程环境中使用,需配合 ConcurrentHashMap 或其他同步机制。
• 性能:在单线程中,相比传统写法,性能差异可以忽略,但代码更简洁。
通过 computeIfAbsent,这行代码优雅地实现了“按需创建列表并添加坐标”的逻辑,是 Java 8 函数式编程的典型应用。
此外,以下是针对三个场景的具体示例,均使用 computeIfAbsent 简化逻辑:
1. 缓存初始化:用户信息缓存
场景:缓存用户信息,避免重复查询数据库。
代码:
Map<String, User> userCache = new HashMap<>();
public User getUserById(String userId) {
// 如果 userId 不在缓存中,从数据库加载并存入缓存
return userCache.computeIfAbsent(userId, k -> database.loadUserById(k));
}
说明:
• database.loadUserById(k) 是模拟从数据库加载用户的逻辑。
• computeIfAbsent 保证每个 userId 只触发一次数据库查询。
2. 按需创建资源:数据库连接池
场景:为不同数据源按需创建连接池。
代码:
Map<String, ConnectionPool> connectionPools = new HashMap<>();
public ConnectionPool getConnectionPool(String dataSourceName) {
return connectionPools.computeIfAbsent(dataSourceName, k -> {
// 首次访问该数据源时,初始化连接池
ConnectionPool pool = new ConnectionPool();
pool.init(k); // 根据数据源配置初始化
return pool;
});
}
说明: • 每个数据源的连接池在第一次使用时初始化。 • 避免提前创建所有连接池,节省资源。
3. 统计分组:订单按类型聚合
场景:将订单列表按类型(如 "food", "electronics")分组统计。
代码:
List<Order> orders = getOrders(); // 获取所有订单
Map<String, List<Order>> ordersByType = new HashMap<>();
for (Order order : orders) {
// 动态创建订单类型的分组列表
ordersByType.computeIfAbsent(order.getType(), k -> new ArrayList<>())
.add(order);
}
说明: • 自动为每种订单类型创建列表,无需提前知道所有可能的类型。 • 替代传统写法:
List<Order> list = ordersByType.get(type);
if (list == null) {
list = new ArrayList<>();
ordersByType.put(type, list);
}
list.add(order);
总结
| 场景 | 核心逻辑 | computeIfAbsent 的作用 |
|---|---|---|
| 缓存初始化 | 避免重复加载数据 | 键不存在时触发初始化逻辑 |
| 按需创建资源 | 懒加载昂贵资源(如连接池) | 资源首次使用时才创建 |
| 统计分组 | 动态聚合数据到分组列表 | 自动为每个键创建容器,简化分组逻辑 |
通过 computeIfAbsent,这些场景的代码更简洁、高效,且避免了冗余的 if (map.containsKey(...)) 检查。