Java Map的computeIfAbsent方法语法是什么样的?

961 阅读4分钟

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});

步骤拆解

  1. 检查键是否存在
    检查 Map cache 中是否存在键 c(字符类型)。

  2. 处理键不存在的情况
    • 如果 c 不存在,调用 mappingFunction(即 k -> new ArrayList<>()),创建一个新的空 ArrayList。 • 将新列表作为值插入 Map,键为 c。 • 返回这个新列表。

  3. 处理键存在的情况
    • 直接返回键 c 对应的现有列表。

  4. 添加坐标到列表
    无论键是否存在,最终都会获得一个列表(可能是新创建的或已存在的),然后将坐标 [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(...)) 检查。