我通常不会在JDK中解释一个方法,但是当我这样做时,它就差不多了Map.merge()
。在键值范围中可能是功能最丰富的操作。而且还比较晦涩,很少使用。merge()
可以解释如下:将新值放在给定键下(如果不存在),或者用给定值更新现有键(UPSERT)。让我们从最基本的示例开始:计算唯一单词的出现次数。Java 8之前的版本(阅读:2014之前!)非常混乱,实现细节中的实质丢失了:
var map = new HashMap <String,Integer>(); words.forEach(word-> { var prev = map.get(word); 如果(上一个== null){ map.put(word,1); }其他{ 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.get(word)
里面map.put()
有点尴尬。让我们也摆脱它!
words.forEach(word-> { map.putIfAbsent(word,0); map.computeIfPresent(word,(w,prev)-> prev + 1); });
computeIfPresent()
仅当word
存在问题()的键时,才调用给定的转换。否则什么都不做。我们通过将密钥初始化为零来确保密钥存在,因此增量始终有效。我们可以做得更好吗?我们可以减少额外的初始化,但是我不建议这样做:
words.forEach(word-> map.compute(word,(w,prev)-> prev!= null?prev + 1:1) );
compute()
类似于computeIfPresent()
,但是无论给定键是否存在都被调用。如果键的值不存在,则prev
参数为null
。将简单的if
表达式转换为隐藏在lambda中的三元表达式远非最佳。这是merge()
操作员大放异彩的地方。在向您展示最终版本之前,让我们来看一个稍微简化的默认实现Map.merge()
:
默认的V合并(K键,V值,BiFunction <V,V,V> remappingFunction){ V oldValue = get(key); V newValue =(oldValue ==空)值: remappingFunction.apply(oldValue,value); 如果(newValue == null){ remove(key); }其他{ put(key,newValue); } 返回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
下word
,否则请添加1
到现有值。我将参数之一命名为“ one
”,因为在我们的示例中,该参数始终为…。1.遗憾地remappingFunction
采用了两个参数,其中第二个是我们即将更新(插入或更新)的值。从技术上讲,我们已经知道此值,因此(word, 1, prev -> prev + 1)
更容易消化。但是没有这样的API。
可以,但是merge()
真的有用吗?假设您有一个帐户操作(省略了构造函数,getter和其他有用的属性):
类别操作{ 私有最终String accNo; 私人最终BigDecimal金额; }
以及针对不同帐户的一系列操作:
var操作= List.of( new Operation(“ 123”,new BigDecimal(“ 10”)), 新操作(“ 456”,新BigDecimal(“ 1200”)), new Operation(“ 123”,new BigDecimal(“-4”)), new Operation(“ 123”,new BigDecimal(“ 8”)), 新操作(“ 456”,新BigDecimal(“ 800”)), 新操作(“ 456”,新BigDecimal(“-1500”)), new Operation(“ 123”,new BigDecimal(“ 2”)), new Operation(“ 123”,new BigDecimal(“-6.5”)), 新操作(“ 456”,新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,金额)-> soFar.add(金额)) );
您在这里看到方法参考机会吗?
Operations.forEach(op-> balances.merge(op.getAccNo(),op.getAmount(),BigDecimal ::添加) );
我觉得这很可读。对于add
给amount
定的每个操作accNo
。结果符合预期:
{123=9.5, 456=-100}
ConcurrentHashMap
Map.merge()
当您意识到它已在中正确实现时,它会更加明亮ConcurrentHashMap
。这意味着我们可以原子地执行插入或更新操作。单行和线程安全。ConcurrentHashMap
显然是线程安全的,但不能跨多个操作使用,例如get()
then put()
。但是merge()
,请确保没有更新丢失。