java8 Map中的computeIfAbsent方法

java8在接口Map中增加了computeIfAbsent方法,可以通过此方法构建本地缓存,降低程序的计算量,程序的复杂度,使代码简洁,易懂。

一、概念及使用介绍

在JAVA8的Map接口中,增加了一个方法computeIfAbsent,此方法签名如下:

public V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)

Map接口的实现类如HashMap,ConcurrentHashMap,HashTable等继承了此方法,通过此方法可以构建JAVA本地缓存,降低程序的计算量,程序的复杂度,使代码简洁,易懂。

此方法首先判断缓存MAP中是否存在指定key的值,如果不存在,会自动调用mappingFunction(key)计算key的value,然后将key = value放入到缓存Map,java8会使用thread-safe的方式从cache中存取记录。

如果mappingFunction(key)返回的值为null或抛出异常,则不会有记录存入map

代码:

/**
 * @author wzx
 * @time 2018/2/8
 */
public class MapTest {
    static Map<Integer, Integer> cache = new ConcurrentHashMap<>();

    public static void main(String[] args) throws InterruptedException {
        cache.put(0, 0);
        cache.put(1, 1);
        // 普通方式
        System.out.println("Fibonacci(7) = " + fibonacci(7));
        // 采用java7的同步线程方式及java8的本地缓存的方式
        System.out.println("FibonacciJava8(7) = " + fibonacciJava8(7));
        System.out.println("FibonacciJava7(7) = " + fibonacciJava7(7));

        // 构建多值Map样例代码
        Map<String, HashSet<String>> map1 = new HashMap<>();
        map1.computeIfAbsent("fruits", k -> genValue(k)).add("apple");
        map1.computeIfAbsent("fruits", k -> genValue(k)).add("orange");
        map1.computeIfAbsent("fruits", k -> genValue(k)).add("pear");
        map1.computeIfAbsent("fruits", k -> genValue(k)).add("banana");
        map1.computeIfAbsent("fruits", k -> genValue(k)).add("water");
        System.out.println(map1);

        //测试多线程并发处理,是否同步操作
        Map<String, String> map2 = new ConcurrentHashMap<>();
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            exec.execute(() -> {
                map2.computeIfAbsent("name", k -> genValue2(k));
                map2.computeIfAbsent("addr", k -> genValue2(k));
                map2.computeIfAbsent("email", k -> genValue2(k));
                map2.computeIfAbsent("mobile", k -> genValue2(k));
            });
        }
        exec.shutdown();
        exec.awaitTermination(1, TimeUnit.SECONDS);
        System.out.println(map2);
    }

    static HashSet<String> genValue(String str) {
        return new HashSet<String>();
    }

    static String genValue2(String str) {
        System.out.println("===");
        return str + "2";
    }

    /**
     * 普通的实现方式 普通方式使用大量的计算,存在性能问题. 并且计算量随着n的增加呈指数级增加,需要用到一些缓存策略,并且是线程安全的.
     *
     * @param n
     * @return
     */
    static int fibonacci(int n) {
        if (n == 0 || n == 1) {
            return n;
        }
        return fibonacci(n - 2) + fibonacci(n - 1);

    }

    /**
     * 在java7中的实现方式
     * 在java7中,通过synchronized进行线程同步,检查缓存是否存在key对应的值,如果不存在才进行计算并放入缓存中
     * 为了更好的性能,需要使用 double-checked locking,那样代码会更复杂
     *
     * @param n
     * @return
     */
    static int fibonacciJava7(int n) {
        if (n == 0 || n == 1) {
            return n;
        }
        Integer result = cache.get(n);
        if (result == null) {
            synchronized (cache) {
                result = cache.get(n);
                if (result == null) {
                    System.out.println("calculating FibonacciJava7(" + n + ")");
                    result = fibonacciJava7(n - 2) + fibonacciJava7(n - 1);
                    cache.put(n, result);
                }
            }
        }
        return result;
    }

    /**
     * 采用java8的本地缓存方式 如果缓存MAP中不存在指定key的值,会自动调用mappingFunction(key)计算key的value
     * 然后将key = value放入到缓存Map,java8会使用thread-safe的方式从cache中存取记录
     *
     * @param n
     * @return
     */
    static int fibonacciJava8(int n) {
        return cache.computeIfAbsent(n, (key) -> {
            System.out.println("calculating FibonacciJava8 " + n);
            return fibonacciJava8(n - 2) + fibonacciJava8(n - 1);
        });
    }
}

输出结果:

Fibonacci(7) = 13
calculating FibonacciJava8 7
calculating FibonacciJava8 5
calculating FibonacciJava8 3
calculating FibonacciJava8 2
calculating FibonacciJava8 4
calculating FibonacciJava8 6
FibonacciJava8(7) = 13
FibonacciJava7(7) = 13
{fruits=[orange, banana, apple, pear, water]}
===
===
===
===
{name=name2, mobile=mobile2, addr=addr2, email=email2}

https://my.oschina.net/cloudcoder/blog/217775

<think>我们正在讨论Java中ConcurrentMapcomputeIfAbsent方法。根据用户的问题,我们需要解释这个方法的使用和注意事项。首先,computeIfAbsentJava8引入的一个非常有用的方法,它用于ConcurrentMap(以及Map)接口。它的作用是在键不存在(或映射为null)时,使用提供的函数计算值并将其放入映射中,然后返回该值。如果键存在,则直接返回对应的值。方法签名:VcomputeIfAbsent(K key, Function<? superK,? extendsV>mappingFunction)使用场景:1.当需要初始化一个复杂值(例如集合)时,可以避免重复检查键是否存在。2.计数器的实现(虽然使用merge方法可能更直接,但也可以用computeIfAbsent结合其他方法实现)。3.惰性初始化。注意事项:-在ConcurrentMap中,这个方法保证原子性。整个操作(检查键是否存在,计算值,放入映射)在并发环境下是原子的。-然而,需要注意的是,传入的mappingFunction应该尽可能简单,避免长时间的计算,因为在计算过程中,该键对应的映射可能会被锁定(具体取决于ConcurrentMap的实现)。在ConcurrentHashMap中,使用段锁或CAS操作,但计算函数执行时,该键所在的桶可能会被锁定,因此长时间的计算会降低并发性能。-另外,mappingFunction不应该尝试修改此映射,因为这将导致未定义的行为(例如,在计算函数中又调用了同一个映射的computeIfAbsent方法,可能会导致死锁或无限递归)。下面通过几个示例来说明:示例1:初始化嵌套集合(如Map<String,List<String>>)ConcurrentMap<String, List<String>>map =new ConcurrentHashMap<>();map.computeIfAbsent("key",k ->new ArrayList<>()).add("value");示例2:计数器(使用computeIfAbsent结合replace或merge)但是注意,对于计数器,使用merge方法更直接:map.merge(key,1,Integer::sum);如果非要用computeIfAbsent,可以这样(但效率可能不如merge):map.computeIfAbsent(key, k-> newAtomicInteger(0)).incrementAndGet();但这样就需要存储AtomicInteger,所以推荐使用merge。示例3:惰性初始化(比如初始化一个昂贵的对象)ConcurrentMap<String, ExpensiveObject> cache= newConcurrentHashMap<>();ExpensiveObject obj= cache.computeIfAbsent("key",k ->new ExpensiveObject());注意事项中的重点:1.原子性:在ConcurrentMap中,这个操作是原子的,但要注意函数内部不要有对同一个映射的修改操作。2.避免递归调用:在mappingFunction中,不要调用同一个映射的computeIfAbsent(或其他修改方法)来尝试修改当前映射,这可能会导致死锁(因为ConcurrentHashMap在计算期间会锁定该桶)或递归调用导致栈溢出(在非并发映射中)。3.性能:计算函数应快速执行,避免阻塞其他线程。特别地,在ConcurrentHashMap中,从Java8开始,实现使用了树和CAS操作,但计算函数执行时,该键所在的节点会被锁定(使用synchronized关键字)。因此,长时间的计算会阻塞其他线程访问该桶(但其他桶不受影响)。另外,在Java9中,ConcurrentHashMapcomputeIfAbsent方法有改进:如果值已经存在,则不会调用计算函数(这看起来是理所当然的,但在Java8中,即使值存在,计算函数也会被调用,但不会使用其结果?实际上,Java8的规范也是只有在键不存在或映射为null时才调用函数。但是,在Java8的实现中有一个bug:如果计算函数返回null,那么映射不会被改变,但该键的映射会被移除(如果之前存在)?实际上,根据规范,如果计算函数返回null,则不会插入映射,并且如果之前没有映射,则返回null。但要注意,在ConcurrentHashMap中,如果计算函数返回null,则相当于没有操作,不会插入任何值。另外,如果键存在且映射为null?ConcurrentHashMap不允许null值,所以这种情况不会发生。因此,在ConcurrentHashMap中,computeIfAbsent只会在键不存在时计算值并插入(非null值),否则返回当前值(非null)。最后,注意:ConcurrentHashMap不允许null键和null值。因此,在computeIfAbsent中,计算函数不能返回null,否则会抛出NullPointerException。总结:-computeIfAbsent是一个原子操作,用于在键不存在时计算并插入值。-在ConcurrentHashMap中,计算函数执行期间,该键所在的桶会被锁定,因此应保持计算函数简短。-避免在计算函数中修改当前映射(尤其是对同一个键的操作),否则可能导致死锁或未定义行为。-计算函数不能返回null,否则会抛出NullPointerException。引用:在之前的引用[1]和[2]中,我们已经看到了一些传统实现和computeIfAbsent的简化方式。这些引用说明了使用computeIfAbsent可以简化代码,并提高可读性。根据以上内容,我们可以组织回答。</think>### Java ConcurrentMap `computeIfAbsent` 方法的使用与注意事项 `computeIfAbsent` 是 `ConcurrentMap` 接口(如 `ConcurrentHashMap`)的核心方法,用于原子性地处理键值对初始化。其签名如下: ```java V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) ``` #### **核心功能** 1. **检查键是否存在**:若键已存在且关联值非 `null`,直接返回当前值。 2. **惰性计算**:若键不存在或值为 `null`,调用 `mappingFunction` 计算新值。 3. **原子写入**:将计算后的值(非 `null`)插入映射并返回。 4. **跳过无效计算**:若 `mappingFunction` 返回 `null`,不插入映射并返回 `null`[^1]。 #### **典型应用场景** 1. **初始化嵌套集合**(避免空指针): ```java ConcurrentMap<String, List<String>> map = new ConcurrentHashMap<>(); map.computeIfAbsent("groupA", k -> new CopyOnWriteArrayList<>()).add("data1"); // 输出:{groupA=[data1]} ``` 2. **缓存惰性加载**: ```java ConcurrentMap<String, ExpensiveResource> cache = new ConcurrentHashMap<>(); ExpensiveResource resource = cache.computeIfAbsent("key", k -> loadResource(k)); ``` 3. **线程安全计数器**(结合 `AtomicInteger`): ```java ConcurrentMap<String, AtomicInteger> counters = new ConcurrentHashMap<>(); counters.computeIfAbsent("apple", k -> new AtomicInteger(0)).incrementAndGet(); ``` #### **关键注意事项** 1. **函数纯度要求**: - `mappingFunction` 必须是**无副作用**的纯函数。 - **禁止在函数内修改当前映射**,否则可能引发死锁或未定义行为[^2]。 ```java // 错误示例:递归调用导致死锁风险 map.computeIfAbsent("A", k -> { map.put("B", "value"); // 危险操作! return "value"; }); ``` 2. **性能优化**: - 计算函数应**轻量快速**,避免阻塞并发线程(`ConcurrentHashMap` 在计算期间锁定哈希桶)[^3]。 - 计数器场景优先使用 `merge` 方法: ```java map.merge(key, 1, Integer::sum); // 比 computeIfAbsent + replace 更高效 ``` 3. **空值约束**: - `ConcurrentHashMap` 禁止 `null` 键或值。 - 若 `mappingFunction` 返回 `null`,抛出 `NullPointerException`。 4. **递归风险**: - 避免在 `mappingFunction` 中调用当前映射的其他方法(如 `put`/`get`),可能导致线程阻塞[^3]。 #### **与 `putIfAbsent` 的区别** | 方法 | 是否触发计算 | 返回值处理 | 适用场景 | |--------------------|--------------|--------------------|------------------------| | `putIfAbsent` | 否 | 直接插入给定值 | 值已存在时需避免覆盖 | | `computeIfAbsent` | 是 | 动态生成值后插入 | 值初始化成本较高时使用 | **示例对比**: ```java // putIfAbsent 需预计算值(可能浪费资源) Value v = new ExpensiveValue(); map.putIfAbsent(key, v); // computeIfAbsent 按需计算(资源高效) map.computeIfAbsent(key, k -> new ExpensiveValue()); ``` ### 总结 `computeIfAbsent` 通过原子化的“检查-计算-写入”机制,显著简化并发环境下的惰性初始化逻辑。使用时需严格遵循: 1. 保持计算函数轻量和无状态 2. 避免嵌套修改当前映射 3. 优先用 `merge` 处理计数器场景 4. 始终处理非空返回值
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值