在整理Java编程拾遗『容器概述』这篇文章时,看到Map接口中在Java8后加入了compute的一系列方法,computeIfAbsent、computeIfPresent以及compute方法。本篇文章就来讲述一下这三个方法的使用。
S.N. | 方法 | 说明 |
---|---|---|
1 | default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) | Java 8新方法,如果key在Map中不存在或者对应的value为null, 则根据Function规则计算key对应的value值, 并进行put操作(前提Function计算得到的value值也不为null) |
2 | default V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) | Java 8新方法,如果key在Map中存在且对应的value不为null, 则根据BiFunction规则通过key和对应的value计算newValue,如果newValue为null, 则删除key对应的键值对,否则替换key对应的value值 |
3 | default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) | Java 8新方法,相当于上述两个方法的综合,根据BiFunction,通过key和对应的value 计算newValue,如果newValue为null,会删除对应KV映射,否则会插入或替换原KV映射 |
Java8提供的这几个方法最大的亮点就是:线程安全。ConcurrentHashMap类中的实现是线程安全的,使用ConcurrentHashMap的上述方法,无论如何放来放去,都能保证是线程安全的。
1. 线程安全
public class MapTest {
private static final Map<String, AtomicInteger> counters = new HashMap<>();
private static List<String> stringList = Lists.newArrayList("zhuo", "li", "zhuo", "li", "zhuo", "test");
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 3; i++) {
exec.execute(() -> accumulate(stringList));
}
exec.shutdown();
exec.awaitTermination(1, TimeUnit.SECONDS);
System.out.println(counters);
}
private static void accumulate(List<String> stringList) {
stringList.forEach(ele -> counters.computeIfAbsent(ele, k -> new AtomicInteger()).incrementAndGet());
}
}
上述代码,构建了一个Map<String, AtomicInteger>的Map,用来统计一个List中各个元素出现的次数。为了测试computeIfAbsent方法的线程安全特性,所以写了上面这个方法。实际开发中肯定不会把Map定义为类的成员变量成为竞态条件,导致线程不安全,而会把Map定义为accumulate方法的局部变量。在上述代码中,定义一个线程池,并向线程池添加三个任务,任务会统计一个List中各个元素的出现次数,将结果写入Map<String, AtomicInteger>中。线程池中的三个任务,相当于对stringList统计了三次,预期结果是:
{zhuo=9, test=3, li=6}
但是上述代码却会有一定的几率出现一些异常结果,比如:
{zhuo=8, test=3, li=5}
说明对于HashMap的computeIfAbsent方法,是线程不安全的(目测应该是两个线程同时满足了Absent条件,之后一个线程的结果,覆盖了另一个线程的结果),不然结果肯定跟预期结果那样,相当于对stringList统计了三次。现在我们将counters定义为ConCurrentHashMap,如下:
private static final Map<String, AtomicInteger> counters = new ConcurrentHashMap<>();
可以发现,无论怎么执行,结果都是:
{zhuo=9, test=3, li=6}
说明ConcurrentHashMap的computeIfAbsent方法是线程安全的,不存在HashMap的上述线程不安全的问题。由于上述线程安全的特性,可以使用ConcurrentHashMap的computeIfAbsent方法,构建缓存。Mybatis、Dubbo中都有使用。
2. computeIfAbsent
上面讲过computeIfAbsent方法作用:如果key在Map中不存在或者对应的value为null,则根据Function规则计算key对应的value值,并进行put操作(前提Function计算得到的value值也不为null)。最常见的用法是根据key构造一个特定的新对象,并作为key的映射值。如下:
map.computeIfAbsent(1, k -> k + "key");
System.out.println(map.get(1)); //1key
第一个参数是key,第二个参数是lambda函数,用于构建key映射的value值。上述执行结果为:如果map中key为1的键值对不存在,就会向map中添加一个key为1,value为1key的键值对。如果不想使用key进行value的计算,可以直接使用putIfAbsent,如下:
map.putIfAbsent(1, "key");
System.out.println(map.get(1)); //key
computeIfAbsent方法的另一个常见用法是构建多值map,Map<K,Collection<V>>,如下:
map.computeIfAbsent(key, k -> new HashSet<V>()).add(v);
3. computeIfPresent
computeIfPresent方法的作用:如果key在Map中存在且对应的value不为null,则根据BiFunction规则通过key和对应的value计算newValue,如果newValue为null,则删除key对应的键值对,否则替换key对应的value值。如下:
Map<Integer, String> map = new HashMap<>();
map.put(1, "test");
map.computeIfPresent(1, (key, value) ->key + value);
System.out.println(map.get(1)); // 1test
4. compute
compute方法的作用:根据BiFunction,通过key和对应的value计算newValue,如果newValue为null且oldValue不为null,会删除对应KV映射,否则会插入或替换原KV映射,其实就相当于上述两个方法的合体。如下:
list.forEach(group -> groupmap.compute(group.getGroupName(), (k, v) -> (v == null) ? group.getLoad() : group.getLoad() + v));
等价于:
list.forEach(group -> {
groupmap.computeIfPresent(group.getGroupName(), (String key, Integer value) -> value + group.getLoad());
groupmap.putIfAbsent(group.getGroupName(), group.getLoad());
});
参考链接: