Traps

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.

Java 8+ (compute methods); Java 21 (core language)

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}
    }
}

Sources