merge()
Insert a value or combine it with the existing one in a single call, for counters, totals and reference counts.
default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)- Added in Java Java 8
Parameters
| Parameter | Type | Description |
|---|---|---|
$key | K | The key to insert or update. |
$value | V | Stored as is when the key is absent; otherwise passed to the function as its second argument. Must not be null. |
$remappingFunction | BiFunction<? 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)throwsNullPointerException, even when the key is absent. Useremoveto delete. - Argument order. The function receives
(oldValue, newValue). For subtraction or string joining the order matters:(a, b) -> a + "," + bappends the new value at the end. - The function is not called for the first value. The first
valueis 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
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.
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.
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: nullCounting 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.
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