Map.merge()
Map.merge()
Map.merge() 在键值范围中可能是功能最丰富的操作。而且还比较晦涩,很少使用。 merge() 可以解释如下:如果键值不存在(if absent),将新值放在给定键下;否则使用给定值更新现有键(UPSERT)。让我们从最基本的示例开始:计算唯一单词的出现次数。Java 8 之前的实现比较麻烦:
1 | var map = new HashMap<String, Integer>(); |
但是,它可以工作,并且对于给定的输入会产生所需的输出:
1 | var words = List.of("Foo", "Bar", "Foo", "Buzz", "Foo", "Buzz", "Fizz", "Fizz"); |
让我们尝试对其进行重构以避免条件逻辑:
1 | words.forEach(word -> { |
真好! putIfAbsent()
必不可少,否则,代码将在首次出现以前未知的单词时中断。另外,我在 map.put()
中发现 map.get(word)
有点别扭。让我们也摆脱它!
1 | words.forEach(word -> { |
仅当存在相关单词时,computeIfPresent()
才调用给定的转换,否则什么都不做。我们通过将键初始化为零来确保键存在,因此后面的增加转换始终有效。我们可以做得更好吗?通过减少额外的初始化,但是我不建议这样做:
1 | words.forEach(word -> |
compute()
与 computeIfPresent()
类似,但是无论给定键是否存在都被调用。如果键的值不存在,则 prev
参数为 null
。将 if 判断隐藏在 lambda 中的三元表达式远非最佳。这是 merge()
运算符的亮点。在向您展示最终版本之前,让我们看一下 Map.merge()
的默认简化实现:
1 | default V merge(K key, V value, BiFunction<V, V, V> remappingFunction) { |
该代码段价值一千个字。 merge()
在两种情况下工作。如果给定的键不存在,它将变成 put(key,value)
。但是,如果给定键已经具有某个值,那么我们的 remappingFunction
会合并(移除)旧的值和给定值。此功能可如下使用:
- 使用新值覆盖旧值:
(old, new) -> new
- 保留旧值:
(old, new) -> old
- 以某种方式合并两者,例如:
(old, new) -> old + new
- 甚至删除旧值:
(old, new) -> null
如您所见,merge() 非常通用。那么我们的问题使用 merge() 怎么做?非常简单:
1 | words.forEach(word -> |
解释如下:如果单词不存在,该单词初始值为 1,否则将 1 添加到现有值。我将参数之一命名为 “one”,因为在我们的示例中,它始终是……1。遗憾的是 remappingFunction
需要两个参数,其中第二个是我们将要更新(新增或修改)的值。从技术上讲,我们已经知道此值,因此 (word, 1, prev -> prev + 1)
将更容易理解。但是没有这样的 API。
好的,但是 merge() 真的有用吗?假设您有一个帐户操作(省略了构造函数,getter 和其他有用的属性):
1 | class Operation { |
以及针对不同帐户的一系列操作:
1 | var operations = List.of( |
我们想计算每个帐户的余额(总操作金额)。没有 merge()
,这将很麻烦:
1 | var balances = new HashMap<String, BigDecimal>(); |
但是在 merge() 的帮助下:
1 | operations.forEach(op -> |
使用方法引用:
1 | operations.forEach(op -> |
我觉得这很可读。对于每个操作,将给定数量添加到给定编号。结果符合预期:
1 | {123=9.5, 456=-100} |
ConcurrentHashMap
当您意识到 ConcurrentHashMap 中正确实现了 Map.merge() 时,它会更加有用。这意味着我们可以原子地执行插入或更新操作。而且线程安全。