Java HashMap merge: Syntax and Use Cases
java hashmap merge: Learn how Java's HashMap.merge combines existing and new values, handles null entries, and simplifies counting and map-merging logic.
When you need to update a map entry based on its current value, the java hashmap merge method is the cleanest tool available. HashMap.merge combines a key's existing value with a new value using a remapping function, and it was added in Java 8 as a default method on the Map interface.
What HashMap.merge Does
The method signature is:
V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)
The method takes three arguments: the key to update, a new value to combine with the existing value, and a BiFunction that receives the old value and the new value and returns the result. The return value is the value now associated with the key, or null if the entry was removed.
The key behavioral rule is that the remapping function is only invoked when the key is already associated with a non-null value. If the key is absent or mapped to null, merge simply inserts the given value without calling the function. This makes merge safe to call on a fresh map without checking whether the key exists first.
The Three Paths Through merge
Every call to merge follows one of three paths.
Key absent or mapped to null. The map associates the key with the given value. The remapping function is not called.
Map<String, Integer> map = new HashMap<>(); map.merge("cpu", 4, Integer::sum); System.out.println(map.get("cpu")); // 4
Key present with a non-null value. The remapping function is called with the old value and the new value. The returned value replaces the existing mapping.
map.merge("cpu", 2, Integer::sum); System.out.println(map.get("cpu")); // 6
Remapping function returns null. The entry is removed from the map entirely. This is useful for deleting entries conditionally during an update.
map.merge("cpu", 100, (old, value) -> null); System.out.println(map.containsKey("cpu")); // false
The remapping function must not modify the map during the merge operation. Doing so can throw a ConcurrentModificationException, since merge performs the computation while the map's structure is being examined.
Practical Examples: Counting and Combining
The most common use of merge is counting occurrences. The classic alternative is a get-and-put sequence that requires a null check on every iteration:
Map<String, Integer> counts = new HashMap<>(); for (String word : words) { counts.put(word, counts.getOrDefault(word, 0) + 1); }
With merge, the same logic becomes a single call:
Map<String, Integer> counts = new HashMap<>(); for (String word : words) { counts.merge(word, 1, Integer::sum); }
When the key is absent, merge inserts 1. When the key is present, it adds 1 to the existing count. The Integer::sum method reference is a natural fit because the remapping function receives the old count and the new value.
Another common pattern is merging two maps into one:
Map<String, Integer> base = new HashMap<>(); Map<String, Integer> overlay = new HashMap<>(); overlay.forEach((key, value) -> base.merge(key, value, Integer::sum));
This accumulates overlay values into base without overwriting existing entries. If a key exists in both maps, the values are summed; if it exists only in overlay, it is inserted unchanged.
merge vs put vs compute
The three methods that update a map entry are easy to confuse. The difference is in how the new value is produced and when the function runs.
| Method | Key absent | Key present | Function arguments |
|---|---|---|---|
| put | Inserts value | Replaces value | none |
| merge | Inserts value | Applies function to old and new value | old value, new value |
| compute | Applies function to key | Applies function to key and value | key, value (or key alone) |
put is the right choice when you simply want to replace the value unconditionally. merge is right when the update depends on the existing value and you want the absent-key case to fall back to a default. compute is right when the new value depends on the key itself, not on a second value you already hold.
A concrete example of compute:
map.compute("session", (key, value) -> value == null ? 1 : value + 1);
This behaves similarly to merge for counting, but the function receives the key and the existing value. merge is usually clearer when you already have a value to combine.
Null Handling in merge
Null values interact with merge in ways that surprise developers coming from put. The rules are:
- If the key is absent and the value argument is null, merge does nothing and returns null. No entry is created.
- If the key is mapped to null, merge treats it as absent and inserts the value argument.
- If the remapping function returns null, the entry is removed.
Map<String, Integer> map = new HashMap<>(); map.put("a", null); map.merge("a", 10, Integer::sum); System.out.println(map.get("a")); // 10 map.merge("missing", null, Integer::sum); System.out.println(map.containsKey("missing")); // false
This null-tolerant behavior is what makes merge safe to call without a preceding containsKey check. It also means you cannot use merge to insert a null value into an empty map; the call is silently ignored.
Concurrency and Thread Safety
HashMap itself is not thread-safe, and merge does not change that. Concurrent modification from multiple threads can corrupt the map's internal structure. If you need merge to run atomically across threads, use ConcurrentHashMap, which implements the same merge method with atomic semantics.
ConcurrentHashMap<String, Long> counters = new ConcurrentHashMap<>(); counters.merge("requests", 1L, Long::sum);
The merge operation on ConcurrentHashMap is performed atomically, so concurrent calls for the same key do not lose updates. This is the standard way to maintain a shared counter or accumulator in a multithreaded context. The remapping function is still executed under the map's internal lock for that bin, so the function should be short and must not call back into the same map.
Performance Characteristics
merge performs at most one lookup for the key, followed by either an insertion or an update. The remapping function is only invoked when the key is already present with a non-null value, so a counting loop over a large dataset pays the function cost only for repeated keys. The get-then-put pattern performs two lookups per entry and requires an explicit null check, so merge is both shorter and slightly cheaper in the common case.
The main performance consideration is the remapping function itself. It runs for every existing key update, so it should be cheap. A function that performs I/O, allocates large objects, or calls into the same map can turn a simple counter into a bottleneck. For the counting use case, Integer::sum is effectively free.