95. Map Compute Patterns vs get-null-put
Replace verbose get-null-check-put patterns with computeIfAbsent, computeIfPresent, compute, merge, and getOrDefault โ they are atomic on ConcurrentHashMap and more readable everywhere.
Explanation
The classic anti-pattern: if (!map.containsKey(k)) { map.put(k, defaultVal); } return map.get(k); โ verbose and not atomic. Modern replacements:
getOrDefault(key, default): returns the value or a default if absent โ does not insert the default.
computeIfAbsent(key, fn): if the key is absent (or mapped to null), compute a value from the key and insert it. Returns the (new or existing) value. Atomic on ConcurrentHashMap โ no additional synchronisation needed for building multi-maps.
computeIfPresent(key, fn): applies a function only if the key is present; removes the mapping if the function returns null.
compute(key, fn): always applies the function (even if absent); if the function returns null, the mapping is removed.
merge(key, value, fn): if the key is absent, insert value; if present, apply fn to the existing and new value.
Example
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class MapComputePatterns {
public static void main(String[] args) {
// getOrDefault
Map<String, Integer> scores = new HashMap<>(Map.of("Alice", 95, "Bob", 87));
int charlieScore = scores.getOrDefault("Charlie", 0);
System.out.println("Charlie: " + charlieScore); // 0 (not inserted)
System.out.println("Map has Charlie: " + scores.containsKey("Charlie")); // false
// computeIfAbsent: build multi-map without null checks
Map<String, List<String>> groups = new HashMap<>();
String[][] people = { {"eng", "Alice"}, {"eng", "Bob"}, {"mkt", "Carol"} };
for (String[] p : people) {
groups.computeIfAbsent(p[0], k -> new ArrayList<>()).add(p[1]);
}
System.out.println("Groups: " + groups); // {eng=[Alice, Bob], mkt=[Carol]}
// merge: word count
String[] words = {"java", "is", "great", "java", "is", "java"};
Map<String, Integer> count = new HashMap<>();
for (String w : words) {
count.merge(w, 1, Integer::sum); // if absent: put 1; if present: add 1
}
System.out.println("Word count: " + count); // {java=3, is=2, great=1}
// compute: update or remove
Map<String, Integer> cache = new HashMap<>(Map.of("a", 5, "b", 3));
cache.compute("a", (k, v) -> v == null ? 1 : v - 1);
cache.compute("b", (k, v) -> v == null ? 1 : (v - 1 == 0 ? null : v - 1));
System.out.println("Cache after compute: " + cache); // {a=4} (b removed when 0)
// ConcurrentHashMap: compute ops are atomic
ConcurrentHashMap<String, Integer> concurrent = new ConcurrentHashMap<>();
concurrent.computeIfAbsent("key", k -> 0);
concurrent.compute("key", (k, v) -> v + 1); // atomic increment
System.out.println("Concurrent: " + concurrent);
// computeIfPresent: only update if key exists
Map<String, String> status = new HashMap<>(Map.of("user1", "active"));
status.computeIfPresent("user1", (k, v) -> v.equals("active") ? "inactive" : v);
status.computeIfPresent("user2", (k, v) -> "never runs"); // user2 absent
System.out.println("Status: " + status); // {user1=inactive}
}
}