merge()

Insert a value or combine it with the existing one in a single call, for counters, totals and reference counts.

Signature
default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)
  • Added in Java Java 8

Parameters

ParameterTypeDescription
$keyKThe key to insert or update.
$valueVStored as is when the key is absent; otherwise passed to the function as its second argument. Must not be null.
$remappingFunctionBiFunction<? super V, ? super V, ? extends V>Called as f(oldValue, value) when the key already has a value. Returning null removes the mapping.

Return value

VThe new value associated with the key, or null if the function returned null and the mapping was removed.

Errors

NullPointerException if value or the function is null, or the key is null and the map does not permit null keys; UnsupportedOperationException if the map is unmodifiable; HashMap throws ConcurrentModificationException if the function modifies the map.

merge(key, value, fn) folds a value into a map. If the key is absent (or mapped to null), it stores value. Otherwise it stores fn.apply(oldValue, value). If fn returns null, the key is removed. The method returns the new value, or null after a removal.

Counting and summing

Counting is one line with a method reference:

counts.merge(status, 1, Integer::sum);
totals.merge(customer, amount, BigDecimal::add);

Compare counts.put(k, counts.getOrDefault(k, 0) + 1): two lookups, and on a shared map two separate steps that can interleave with another thread. merge does the read and the write together, and on ConcurrentHashMap it is atomic per key, which makes it a simple way to count requests per endpoint or errors per client from many threads. For very hot counters, a LongAdder created with computeIfAbsent scales better under contention.

Removing by returning null

The function can delete the entry by returning null. That suits reference counts: add 1 when a connection opens, -1 when it closes, and return null when the count reaches zero so the map does not fill with zeros.

Things that trip people up

  • The value cannot be null. merge(k, null, fn) throws NullPointerException, even when the key is absent. Use remove to delete.
  • Argument order. The function receives (oldValue, newValue). For subtraction or string joining the order matters: (a, b) -> a + "," + b appends the new value at the end.
  • The function is not called for the first value. The first value is stored as is, so a merge that "decrements" an absent key stores the negative delta.
  • No changes to the same map inside the function, for the same reason as computeIfAbsent.

In stream code, Collectors.toMap(keyFn, valueFn, mergeFn) and Collectors.groupingBy with counting() apply the same idea to a whole stream.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Counting responses per HTTP status code for a metrics endpoint.
  • Summing order totals per customer from rows read out of a database.
  • Tracking open connections per backend and dropping the entry when the count reaches zero.

Frequently asked questions

How do I count occurrences with a Map in Java?

Call map.merge(item, 1, Integer::sum) for each item. The first occurrence stores 1 and every later one adds 1 to the existing count.

What is the difference between merge and compute?

merge takes a value and only calls the function when the key already has one. compute always calls its function with the key and the current value, which may be null, so you handle the absent case yourself.

Why does Map.merge throw NullPointerException?

The value argument is null. merge does not accept a null value even for an absent key. To remove a key, return null from the remapping function or call remove.

More examples

Summing money per customer

BigDecimal::add combines the existing total with the new amount, and the first payment for each customer is stored as is. BigDecimal keeps the cents exact, which a double would not.

Example · java
record Payment(String customer, BigDecimal amount) {}

void main() {
    List<Payment> payments = List.of(
            new Payment("acme", new BigDecimal("19.99")),
            new Payment("globex", new BigDecimal("5.00")),
            new Payment("acme", new BigDecimal("0.01")));

    Map<String, BigDecimal> totals = new TreeMap<>();
    for (Payment p : payments) {
        totals.merge(p.customer(), p.amount(), BigDecimal::add);
    }
    IO.println(totals);
}
// Output:
// {acme=20.00, globex=5.00}

Reference counts that remove themselves at zero

When the count reaches zero the function returns null, so merge removes the key and returns null. The map holds only resources that are actually in use.

Example · java
void main() {
    BiFunction<Integer, Integer, Integer> add =
            (current, delta) -> current + delta == 0 ? null : current + delta;

    Map<String, Integer> openConnections = new TreeMap<>();
    openConnections.merge("db-primary", 1, add);
    openConnections.merge("db-primary", 1, add);
    openConnections.merge("cache", 1, add);
    IO.println(openConnections);

    openConnections.merge("db-primary", -1, add);
    Integer cacheNow = openConnections.merge("cache", -1, add);
    IO.println(openConnections);
    IO.println("cache returned: " + cacheNow);
}
// Output:
// {cache=1, db-primary=2}
// {db-primary=1}
// cache returned: null

Counting hits from many threads

Each merge on ConcurrentHashMap is atomic, so no increment is lost even with a thousand concurrent tasks. The same code with get and put could lose updates.

Example · java
void main() {
    Map<String, Integer> hits = new ConcurrentHashMap<>();

    try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
        for (int i = 0; i < 1_000; i++) {
            String path = (i % 4 == 0) ? "/login" : "/products";
            pool.submit(() -> hits.merge(path, 1, Integer::sum));
        }
    } // close() waits for every task

    IO.println(new TreeMap<>(hits));
}
// Output:
// {/login=250, /products=750}

Discussion

  • Be the first to comment on this lesson.