使用Map.merge()替代ConcurrentHashMap

banq發表於2019-03-10

Map.merge()意味著我們可以原子地執行插入或更新操作,它是執行緒安全的,ConcurrentHashMap雖然也是執行緒安全的,但不是所有操作都是,例如get()之後再put()就不是了,這時使用merge()確保沒有更新會丟失。

 Map.merge()可以解釋如下:它將新值置於指定的key鍵下(如果不存在)或更新具有給定值的現有鍵(UPSERT)。讓我們從最基本的例子開始:計算唯一的單詞出現次數。

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.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 != null ? prev + 1 : 1)
);


compute()類似computeIfPresent(),但無論是否存在指定的key,都會呼叫它。如果key的值不存在,則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()適用於兩種情況。如果給定的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和其他有用的屬性省略):

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}

 

相關文章