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