coding……
但行好事 莫问前程

Java8 Map coumpute操作

在整理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());
});

参考链接:

  1. Java8(3):Java8 中 Map 接口的新方法
  2. How do I use the new computeIfAbsent function?
  3. Java API

赞(0) 打赏
Zhuoli's Blog » Java8 Map coumpute操作
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址