Advanced Java Map API: computeIfAbsent, merge, getOrDefault & More

Java 8 added a rich set of default methods to the Map interface that replace verbose get-check-put patterns with concise, atomic alternatives. getOrDefault avoids null checks for missing keys; putIfAbsent prevents overwriting existing values; computeIfAbsent initialises missing entries on first access; merge combines existing and new values in one call; replaceAll transforms every value in-place. This article covers all of them with practical examples including frequency maps, grouped data, and cache-style patterns.

getOrDefault and putIfAbsent: Safe Reads and Conditional Writes

The most common pattern before Java 8 was checking whether a key exists before reading or writing: if (map.containsKey(k)) { ... } else { map.put(k, default); }. This is three lines for what should be one, and the two-call pattern is not atomic: in multi-threaded code, another thread can modify the map between the check and the put. Java 8's default methods perform the check and the action atomically within a single call.

getOrDefault(key, defaultValue) returns the mapped value if the key exists, otherwise returns defaultValue without modifying the map. putIfAbsent(key, value) inserts the key-value pair only when the key is not already present, returning the existing value (or null) without overwriting it.

Java
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 90);
scores.put("Bob", 85);

// getOrDefault — read with a fallback for missing keys
int alice  = scores.getOrDefault("Alice", 0);  // 90  (key present)
int carol  = scores.getOrDefault("Carol", 0);  // 0   (key absent — map unchanged)
System.out.println(alice + ", " + carol);

// putIfAbsent — insert only if key is not already mapped
scores.putIfAbsent("Dave", 75);    // inserts Dave=75
scores.putIfAbsent("Alice", 100);  // does nothing — Alice=90 already present
System.out.println(scores.get("Dave"));  // 75
System.out.println(scores.get("Alice")); // 90 — not overwritten

computeIfAbsent: Lazy Initialisation of Map Entries

computeIfAbsent(key, mappingFunction) solves the "initialise on first access" pattern. If the key is absent (or mapped to null), it calls the mapping function to compute a value, stores it, and returns it. If the key is already present, it returns the existing value without calling the function. This is particularly powerful for building maps-of-lists or maps-of-sets, where each access to a new key should create an empty collection automatically.

Java
// Classic pattern — verbose and requires null check
Map<String, List<String>> old = new HashMap<>();
String key = "fruits";
if (!old.containsKey(key)) {
    old.put(key, new ArrayList<>());
}
old.get(key).add("apple");

// computeIfAbsent — clean single-call pattern
Map<String, List<String>> groups = new HashMap<>();
groups.computeIfAbsent("fruits", k -> new ArrayList<>()).add("apple");
groups.computeIfAbsent("fruits", k -> new ArrayList<>()).add("banana"); // reuses existing list
groups.computeIfAbsent("veggies", k -> new ArrayList<>()).add("carrot");
System.out.println(groups);
// {fruits=[apple, banana], veggies=[carrot]}

// Grouping by first letter
List<String> words = List.of("ant", "bear", "ape", "bee", "cat");
Map<Character, List<String>> byLetter = new HashMap<>();
for (String word : words) {
    byLetter.computeIfAbsent(word.charAt(0), k -> new ArrayList<>()).add(word);
}
System.out.println(byLetter);
// {a=[ant, ape], b=[bear, bee], c=[cat]}

// Memoisation / simple cache
Map<Integer, Long> fibCache = new HashMap<>();
fibCache.put(0, 0L);
fibCache.put(1, 1L);

// Iterative Fibonacci using computeIfAbsent as a cache
for (int i = 2; i <= 10; i++) {
    final int n = i;
    fibCache.computeIfAbsent(n, k -> fibCache.get(k-1) + fibCache.get(k-2));
}
System.out.println(fibCache.get(10)); // 55
Do not recursively call computeIfAbsent on the same map inside the mapping function: the behaviour is undefined and may cause an infinite loop or ConcurrentModificationException.

merge: Combining Existing and New Values

merge(key, value, remappingFunction) is the most versatile Map method added in Java 8. It works in three steps: if the key is absent (or mapped to null), it inserts the provided value directly. If the key is present, it calls the remapping function with the old value and the new value to produce a combined result, and if the remapping function returns null, the key is removed. This makes merge ideal for counters, accumulators, and any pattern where you want "insert if new, combine if existing".

Java
// Frequency counter — count occurrences of each word
List<String> words = List.of("cat", "dog", "cat", "bird", "dog", "cat");

Map<String, Integer> freq = new HashMap<>();
for (String word : words) {
    freq.merge(word, 1, Integer::sum);
    // If absent: put (word, 1)
    // If present: put (word, oldCount + 1)
}
System.out.println(freq); // {cat=3, dog=2, bird=1}

// Building a combined string per group
List<String> items = List.of("a:1", "b:2", "a:3", "b:4", "c:5");
Map<String, String> combined = new HashMap<>();
for (String item : items) {
    String[] parts = item.split(":");
    combined.merge(parts[0], parts[1], (old, val) -> old + "," + val);
}
System.out.println(combined); // {a=1,3, b=2,4, c=5}

// Remove entry when remapping function returns null
Map<String, Integer> stock = new HashMap<>(Map.of("apples", 5, "bananas", 3));
// Decrement stock; remove entry when it reaches 0
stock.merge("apples", -2, Integer::sum); // 5 + (-2) = 3
stock.merge("bananas", -3, (old, v) -> (old + v <= 0) ? null : old + v);
System.out.println(stock); // {apples=3} — bananas removed (reached 0)

compute and computeIfPresent: Full Control Over Updates

compute(key, remappingFunction) is the most general form: it calls the remapping function regardless of whether the key exists, passing the key and the current value (or null if absent). If the function returns null, the entry is removed. computeIfPresent(key, remappingFunction) is like compute but only runs the function if the key is currently mapped to a non-null value.

Java
Map<String, Integer> map = new HashMap<>(Map.of("a", 1, "b", 2));

// compute — called whether or not key exists
map.compute("a", (k, v) -> v == null ? 10 : v * 2); // a was 1 → becomes 2
map.compute("c", (k, v) -> v == null ? 99 : v * 2); // c was absent → becomes 99
System.out.println(map); // {a=2, b=2, c=99}

// compute returning null removes the entry
map.compute("b", (k, v) -> null);
System.out.println(map.containsKey("b")); // false

// computeIfPresent — only updates existing keys, ignores absent ones
Map<String, Integer> prices = new HashMap<>(Map.of("milk", 2, "bread", 3));
prices.computeIfPresent("milk",  (k, v) -> v + 1);   // milk: 2 → 3
prices.computeIfPresent("juice", (k, v) -> v + 1);   // juice absent — nothing happens
System.out.println(prices); // {milk=3, bread=3}

replaceAll: Transform Every Value In-Place

replaceAll(BiFunction<K, V, V> function) iterates every entry in the map and replaces each value with the result of the function. It is the map equivalent of List.replaceAll(). The function receives the key and the current value and returns the new value. It cannot add or remove entries, only replace values, so the map's key set and size remain unchanged.

Java
Map<String, Integer> scores = new HashMap<>(Map.of(
    "Alice", 80, "Bob", 70, "Carol", 90
));

// Apply 10% bonus to every score
scores.replaceAll((name, score) -> (int)(score * 1.1));
System.out.println(scores); // {Alice=88, Bob=77, Carol=99}

// Normalise string values to uppercase
Map<String, String> config = new HashMap<>(Map.of(
    "mode", "debug", "env", "production"
));
config.replaceAll((k, v) -> v.toUpperCase());
System.out.println(config); // {mode=DEBUG, env=PRODUCTION}
For thread-safe maps, all these methods (computeIfAbsent, merge, etc.) are available on ConcurrentHashMap and are implemented atomically, meaning the read-modify-write cycle cannot be interrupted by another thread, even though ConcurrentHashMap does not use a global lock.