资讯专栏INFORMATION COLUMN

详解Map.merge()

Ocean / 1115人阅读

摘要:今天介绍的方法,让我们来看看它的强大之处。这可能是中最通用的操作。我们通过将初始化为零来确保存在,因此增量始终有效。这样的搭配场景是对于那些自动执行插入或者更新操作的单线程安全的逻辑。

今天介绍Map的merge方法,让我们来看看它的强大之处。

在JDK的API中,这样的一个方法它是很特别的,它很新颖,它是值得我们花时间去了解的,同时也推荐你可以运用到实际的项目代码中,对你们应该帮助很大。Map.merge())。这可能是Map中最通用的操作。但它也相当模糊,几乎很少人会去使用它。

背景介绍

merge() 可以解释如下:它将新的值赋值给到key中(如果不存在)或更新具有给定值的现有key(UPSERT)。让我们从最基本的例子开始:计算唯一的单词出现次数。在java8之前的时候,代码非常混乱,实际的实现其实已经失去了本质层面的设计意义。

var map = new HashMap();
words.forEach(word -> {
    var prev = map.get(word);
    if (prev == null) {
        map.put(word, 1);
    } else {
        map.put(word, prev + 1);
    }
});

按照上述代码的逻辑,假设给定一个输入集合,输出的结果如下;

var words = List.of("Foo", "Bar", "Foo", "Buzz", "Foo", "Buzz", "Fizz", "Fizz");
//...
{Bar=1, Fizz=2, Foo=3, Buzz=2}
改进V1

现在让我们来重构它,主要去掉它的一些判断逻辑;

words.forEach(word -> {
    map.putIfAbsent(word, 0);
    map.put(word, map.get(word) + 1);
});

这样的改进,是可以满足我们的重构要求。putIfAbsent()的具体用法就不过多描述。putIfAbsent那一行代码是一定需要的,否则,后面的逻辑也就会报错。而在下面代码中,又出现了putget这一点会很奇怪,让我们再继续的进行改进设计。

改进V2
words.forEach(word -> {
    map.putIfAbsent(word, 0);
    map.computeIfPresent(word, (w, prev) -> prev + 1);
});

computeIfPresent是仅当 word中的的key存在的时候才调用给定的转换。否则它什么都不处理。我们通过将key初始化为零来确保key存在,因此增量始终有效。这样的实现是不是已经足够完美?未必,还有其他的思路可以减少额外的初始化。

words.forEach(word ->
        map.compute(word, (w, prev) -> prev != null ? prev + 1 : 1)
);

compute ()就像是computeIfPresent(),但无论给定key的存在与否如何都会调用它。如果key的值不存在,则prev参数为null。将简单移动if 到隐藏在lambda中的三元表达式也远远没有达到最佳的表现。在我向你展示最终版本之前,让我们看一下稍微简化的默认实现Map.merge()源码分析。

改进V3
merge()源码
default V merge(K key, V value, BiFunction remappingFunction) {
    V oldValue = get(key);
    V newValue = (oldValue == null) ? value :
               remappingFunction.apply(oldValue, value);
    if (newValue == null) {
        remove(key);
    } else {
        put(key, newValue);
    }
    return newValue;
}

代码片段胜过千言万语。 阅读源码总是能够发现新大陆,merge() 适用于两种情况。如果给定的key不存在,它就变成了put(key, value)。但是,如果key已经存在一些值,我们 remappingFunction 可以选择合并的方式。这个功能是完美契机上面的场景:

只需返回新值即可覆盖旧值: (old, new) -> new

只需返回旧值即可保留旧值: (old, new) -> old

以某种方式合并两者,例如: (old, new) -> old + new

甚至删除旧值: (old, new) -> null

如你所见,它 merge() 是非常通用的。那么,我们的问题该如何使用merge()呢?代码如下:

words.forEach(word ->
        map.merge(word, 1, (prev, one) -> prev + one)
);

你可以按照如下思路理解:如果没有key,那么初始化的value等于1;否则,将1添加到现有值。代码中的 one 是一个常量,因为我们的场景中,默认一直是加1,具体变化可以随意切换。

场景
想象一下,merge()真的那么好用吗?它的场景可以有什么?

举一个例子。你有一个帐户操作类

class Operation {
    private final String accNo;
    private final BigDecimal amount;
}

以及针对不同帐户的一系列操作:

operations = List.of(
    new Operation("123", new BigDecimal("10")),
    new Operation("456", new BigDecimal("1200")),
    new Operation("123", new BigDecimal("-4")),
    new Operation("123", new BigDecimal("8")),
    new Operation("456", new BigDecimal("800")),
    new Operation("456", new BigDecimal("-1500")),
    new Operation("123", new BigDecimal("2")),
    new Operation("123", new BigDecimal("-6.5")),
    new Operation("456", new BigDecimal("-600"))
);

我们希望为每个帐户计算余额(总运营金额)。假如不用merge(),就变得非常麻烦了:

Map balances = new HashMap();
operations.forEach(op -> {
    var key = op.getAccNo();
    balances.putIfAbsent(key, BigDecimal.ZERO);
    balances.computeIfPresent(key, (accNo, prev) -> prev.add(op.getAmount()));
});

使用merge之后的代码

operations.forEach(op ->
        balances.merge(op.getAccNo(), op.getAmount(), 
                (soFar, amount) -> soFar.add(amount))
);

再进行优化的逻辑。

operations.forEach(op ->
        balances.merge(op.getAccNo(), op.getAmount(), BigDecimal::add)
);

当然结果是正确的,这样简洁的代码心动吗?对于每个操作,add在给定的amount给定accNo

{ 123 = 9.5,456 = - 100 }
ConcurrentHashMap

当我们再延伸到ConcurrentHashMap来,当 Map.merge的出现,和ConcurrentHashMap的结合那是非常的完美的。这样的搭配场景是对于那些自动执行插入或者更新操作的单线程安全的逻辑。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73832.html

相关文章

  • java集合-Map

    摘要:增强的集合都可以是任何引用类型的数据,的不允许重复即同一个对象的任何两个通过方法比较总是返回。的这些实现类和子接口中集的存储形式和对应集合中元素的存储形式完全相同。根据的自然顺序,即枚举值的定义顺序,来维护对的顺序。 Java8增强的Map集合 Key-value都可以是任何引用类型的数据,Map的Key不允许重复即同一个Map对象的任何两个key通过equals方法比较总是返回...

    Little_XM 评论0 收藏0
  • Java 8 并发教程:原子变量和 ConcurrentMa

    摘要:并发教程原子变量和原文译者飞龙协议欢迎阅读我的多线程编程系列教程的第三部分。如果你能够在多线程中同时且安全地执行某个操作,而不需要关键字或上一章中的锁,那么这个操作就是原子的。当多线程的更新比读取更频繁时,这个类通常比原子数值类性能更好。 Java 8 并发教程:原子变量和 ConcurrentMap 原文:Java 8 Concurrency Tutorial: Synchroni...

    bitkylin 评论0 收藏0
  • Java实战之Java8指南

    摘要:首先我们定义一个有两个不同控制器的然后,我们创建一个特定的工厂接口来创建新的对象不需要手动的去继承实现该工厂接口,我们只需要将控制器的引用传递给该接口对象就好了的控制器会自动选择合适的构造器方法。这种指向时间轴的对象即是类。 本文为翻译文章,原文地址 这里 欢迎来到本人对于Java 8的系列介绍教程,本教程会引导你一步步领略最新的语法特性。通过一些简单的代码示例你即可以学到默认的接口方...

    nemo 评论0 收藏0
  • React系列之 Immutable

    摘要:原文地址什么是是指一旦被创建就不可以被改变的数据,通过使用不可变数据可以让我们很方便的去处理数据的状态变化检测等问题,而且让我们的程序变得更加的可预见怎么用大体使用深度转换和为和浅转换给倒数第一个赋值更多可以查看这里为什么要用其实从上面 原文地址:https://gmiam.com/post/react-... 什么是 Immutable Data ? Immutable Data 是...

    cc17 评论0 收藏0
  • 多线程

    摘要:线程启动后系统就自动调用方法。守护线程的使用必须在之前设置,否则会跑出一个异常。你不能把正在运行的常规线程设置为守护线程。在线程中产生的新线程也是的。线程的同步控制使用方法可以释放对象锁,使用或可以让等待的一个或所有线程进入就绪状态。 线程的创建 线程:程序中单个顺序的流控制称为线程 一个进程中可以含有多个线程 在操作系统中可以查看线程数 如:在Windows中,在任务管理器,右键,...

    Scholer 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<