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); }
getOrDefault(key, defaultValue)defaultValueputIfAbsent(key, value)null
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)null
// 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
computeIfAbsentConcurrentModificationExceptionmerge: Combining Existing and New Values
merge(key, value, remappingFunction)nullvaluenullmerge
// 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)nullnullcomputeIfPresent(key, remappingFunction)compute
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)List.replaceAll()
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}
computeIfAbsentmergeConcurrentHashMapConcurrentHashMap