詳解Map.merge()

今天介紹Map的merge方法,讓咱們來看看它的強大之處。

在JDK的API中,這樣的一個方法它是很特別的,它很新穎,它是值得咱們花時間去了解的,同時也推薦你能夠運用到實際的項目代碼中,對大家應該幫助很大。Map.merge())。這多是Map中最通用的操做。但它也至關模糊,幾乎不多人會去使用它。html

背景介紹

merge() 能夠解釋以下:它將新的值賦值給到key中(若是不存在)或更新具備給定值的現有key(UPSERT)。讓咱們從最基本的例子開始:計算惟一的單詞出現次數。在java8以前的時候,代碼很是混亂,實際的實現其實已經失去了本質層面的設計意義。java

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);
    }
});

按照上述代碼的邏輯,假設給定一個輸入集合,輸出的結果以下;api

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

改進V1

如今讓咱們來重構它,主要去掉它的一些判斷邏輯;安全

words.forEach(word -> {
    map.putIfAbsent(word, 0);
    map.put(word, map.get(word) + 1);
});

這樣的改進,是能夠知足咱們的重構要求。putIfAbsent()的具體用法就不過多描述。putIfAbsent那一行代碼是必定須要的,不然,後面的邏輯也就會報錯。而在下面代碼中,又出現了putget這一點會很奇怪,讓咱們再繼續的進行改進設計。oracle

改進V2

words.forEach(word -> {
    map.putIfAbsent(word, 0);
    map.computeIfPresent(word, (w, prev) -> prev + 1);
});

computeIfPresent是僅當 word中的的key存在的時候才調用給定的轉換。不然它什麼都不處理。咱們經過將key初始化爲零來確保key存在,所以增量始終有效。這樣的實現是否是已經足夠完美?未必,還有其餘的思路能夠減小額外的初始化。app

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

compute ()就像是computeIfPresent(),但不管給定key的存在與否如何都會調用它。若是key的值不存在,則prev參數爲null。將簡單移動if 到隱藏在lambda中的三元表達式也遠遠沒有達到最佳的表現。在我向你展現最終版本以前,讓咱們看一下稍微簡化的默認實現Map.merge()源碼分析。源碼分析

改進V3

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() 是很是通用的。那麼,咱們的問題該如何使用merge()呢?代碼以下:spa

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

你能夠按照以下思路理解:若是沒有key,那麼初始化的value等於1;不然,將1添加到現有值。代碼中的 one 是一個常量,由於咱們的場景中,默認一直是加1,具體變化能夠隨意切換。線程

場景

想象一下, merge()真的那麼好用嗎?它的場景能夠有什麼?

舉一個例子。你有一個賬戶操做類

class Operation {
    private final String accNo;
    private final BigDecimal amount;
}

以及針對不一樣賬戶的一系列操做:

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(),就變得很是麻煩了:

Map 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)
);

固然結果是正確的,這樣簡潔的代碼心動嗎?對於每一個操做,add在給定的amount給定accNo

{ 123 = 9.5,456 = - 100 }

ConcurrentHashMap

當咱們再延伸到ConcurrentHashMap來,當 Map.merge的出現,和ConcurrentHashMap的結合那是很是的完美的。這樣的搭配場景是對於那些自動執行插入或者更新操做的單線程安全的邏輯。

關注油膩的Java

相關文章
相關標籤/搜索