Today we introduce the Map merge method to see how powerful it is.

In the JDK API, such a method it is very special, it is new, it is worth our time to learn, but also recommended that you can use the actual project code, should help you a lot. The Map. The merge (). This is probably the most common operation in the Map. But it’s also pretty vague, and very few people use it.

background

Merge () can be interpreted as follows: It assigns a new value to the key (if none exists) or updates an existing key with a given value (UPSERT). Let’s start with the most basic example: counting unique word occurrences. Prior to java8, the code was so confusing that the actual implementation lost its essential design significance.

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); }});Copy the code

Following the logic of the code above, given a set of inputs, the output is as follows;

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

Improve the V1

Now let’s reconstruct it, mainly by removing some of its judgment logic;

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

Such improvements can meet our refactoring requirements. The specific use of putIfAbsent() is more descriptive than that. The putIfAbsent line is definitely required, otherwise the logic will report an error. In the following code, it will be strange to have put and get again, so let’s continue to improve the design.

Improved V2

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

Copy the code

ComputeIfPresent calls the given transformation only if the key in Word exists. Otherwise it does nothing. We ensure that the key exists by initializing it to zero, so the increment is always valid. Is this implementation perfect enough? Not necessarily. There are other ways to reduce the extra initialization.

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

Compute () is like computeIfPresent(), but is called regardless of the presence or absence of a given key. If the key value does not exist, the prev parameter is null. Moving a simple if to a ternary expression hidden in a lambda is also far from optimal performance. Before I show you the final version, let’s take a look at the source code analysis for the slightly simplified default implementation, Map.Merge ().

To improve the V3

Merge () the source code

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;
}
Copy the code

A snippet is worth a thousand words. It is always possible to discover new things by reading source code, and merge() works in both cases. If the given key does not exist, it becomes put(key, value). However, if the key already has some values, we remappingFunction can choose how to merge. This feature is perfect for turning the scene above:

  • Simply return the new value to override the old value:(old, new) -> new
  • Simply return the old value to preserve the old value:(old, new) -> old
  • Combine the two in some way, for example:(old, new) -> old + new
  • Even delete old values:(old, new) -> null

As you can see, it merge() is very generic. So, how do we use merge() in our problem? The code is as follows:


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

If there is no key, then the initialized value is equal to 1. Otherwise, add 1 to the existing value. The one in the code is a constant, because in our scenario, the default is always plus one, and you can switch around.

scenario

Imagine that merge() really works that well? What can its scenes be?

Let me give you an example. You have an account action class

class Operation {
    private final String accNo;
    private final BigDecimal amount;
}
Copy the code

And a series of actions for different accounts:


operations = List.of(
    new Operation("123", new BigDecimal("10")),
    new Operation("456", new BigDecimal("1200")),
    new Operation("123", new BigDecimal("To 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")));Copy the code

We want to calculate the balance (total operating amount) for each account. If you do not merge(), this becomes very cumbersome:


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

Copy the code

The code after using merge

operations.forEach(op ->
        balances.merge(op.getAccNo(), op.getAmount(), 
                (soFar, amount) -> soFar.add(amount))
);


Copy the code

And then optimize the logic.

operations.forEach(op ->
        balances.merge(op.getAccNo(), op.getAmount(), BigDecimal::add)
);
Copy the code

Of course the result is correct, such a simple code heartbeat? For each operation, add gives accNo at the given amount.

{123 = 9.5, 456 = -100}Copy the code

ConcurrentHashMap

When we extend to ConcurrentHashMap, when map. merge comes in, the combination of ConcurrentHashMap is perfect. Such tie-in scenarios are safe for single-thread logic that automatically performs insert or update operations.