使用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}