转载

使用Map.merge()替代ConcurrentHashMap

Map.merge()意味着我们可以原子地执行插入或更新操作,它是线程安全的,ConcurrentHashMap虽然也是线程安全的,但不是所有操作都是,例如get()之后再put()就不是了,这时使用merge()确保没有更新会丢失。

Map.merge() 可以解释如下:它将新值置于指定的key键下(如果不存在)或更新具有给定值的现有键(UPSERT)。让我们从最基本的例子开始:计算唯一的单词出现次数。

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

运行结果:

<b>var</b> words = List.of(<font>"Foo"</font><font>, </font><font>"Bar"</font><font>, </font><font>"Foo"</font><font>, </font><font>"Buzz"</font><font>, </font><font>"Foo"</font><font>, </font><font>"Buzz"</font><font>, </font><font>"Fizz"</font><font>, </font><font>"Fizz"</font><font>);
</font><font><i>//...</i></font><font>
{Bar=1, Fizz=2, Foo=3, Buzz=2}
</font>

避免条件逻辑重构:

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()仅当question(word)中的键key存在时才调用给定的转换。否则什么都不做。我们通过将key初始化为零确保key存在,因此增量始终有效。

我们可以做得更好吗?我们可以削减额外的初始化,但我不推荐它:

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

compute()类似computeIfPresent(),但无论是否存在指定的key,都会调用它。如果key的值不存在,则prev参数为null。

移动简单的if语句到隐藏在lambda中的三元表达式远非最优。

这就是merge()闪耀的地方。在我向您展示最终版本之前,让我们看一下稍微简化的默认实现Map.merge():

<b>default</b> V merge(K key, V value, BiFunction<V, V, V> remappingFunction) {
    V oldValue = get(key);
    V newValue = (oldValue == <b>null</b>) ? value :
               remappingFunction.apply(oldValue, value);
    <b>if</b> (newValue == <b>null</b>) {
        remove(key);
    } <b>else</b> {
        put(key, newValue);
    }
    <b>return</b> newValue;
}

merge()适用于两种情况。如果给定的key不存在,它就变成了put(key, value)。但是,如果key已经有一些值,我们remappingFunction可以合并旧的。这个功能是免费的:

  • 只需返回新值即可覆盖旧值: (old, new) -> new
  • 只需返回旧值即可保留旧值: (old, new) -> old
  • 以某种方式合并两者,例如: (old, new) -> old + new
  • 甚至删除旧值: (old, new) -> null

下面是merge解决我们的案例:

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

如果word这个key不存在则1置其下,否则添加1到现有值。我将其中一个参数命名为“ one”,因为在我们的示例中它始终是...... 1.

遗憾地,remappingFunction需要两个参数,其中第二个是我们即将要插入的值(插入或更新)。从技术上讲,我们已经知道这个值,因此(word, 1, prev -> prev + 1)更容易弄懂,但是没有这样的API。

好的,但merge() 真的有用吗?想象一下,你有一个帐户操作(构造函数,getter和其他有用的属性省略):

<b>class</b> Operation {
    <b>private</b> <b>final</b> String accNo;
    <b>private</b> <b>final</b> BigDecimal amount;
}

不同账户的操作:

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

我们希望为每个帐户计算余额(总金额)。没有merge()这个是非常麻烦的:

var balances = new HashMap<String, BigDecimal>();

operations.forEach(op -> {
    <b>var</b> 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}

原文  https://www.jdon.com/51887
正文到此结束
Loading...