skip to content

What does computeIfAbsent do, and why is it the idiomatic way to build multimaps (Map of List)?

level: middleimportance: must knowfreq 75%

answer

  1. computeIfAbsent: get-or-create-and-store, lazy
  2. multimap: computeIfAbsent(k, x -> new ArrayList<>()).add(v)
  3. created exactly once per key
  4. don't modify other keys inside the function
  5. null result => no entry created

basics

~20 s

computeIfAbsent(key, fn) returns the existing value for key, or if it is missing, runs fn to create a value, stores it, and returns it. It is perfect for grouping: it makes the empty list once, then you add to it.

solid answer

~40 s

`computeIfAbsent(key, mappingFunction)` checks the map: if `key` already has a non-null value it returns that and does nothing else; if absent (or null), it calls `mappingFunction.apply(key)`, stores the result (unless the result is null), and returns it. The mapping function is **lazy** — invoked only on a miss — and receives the key. The canonical use is the 'group-by' / multimap pattern: `map.computeIfAbsent(k, key -> new ArrayList<>()).add(item);`. This collapses the get-null-check-create-put-then-add idiom into one line and guarantees the list is created exactly once per key. Caveats: do not structurally modify the same map *inside* the mapping function (undefined/ConcurrentModificationException), and on `ConcurrentHashMap` the function may run under a lock per bin, so keep it short and non-blocking. If the function returns null, no mapping is created.

go deeper

for a junior

Recognizes computeIfAbsent as get-or-create and can write the multimap one-liner.

for a middle

Explains lazy invocation, exactly-once creation, and why getOrDefault is wrong for grouping.

for a senior

Discusses the no-self-modification rule and ConcurrentHashMap atomicity/locking implications.

for a principal

Reasons about deadlock risks with nested computeIfAbsent on concurrent maps and chooses data structures to keep the mapping function cheap and reentrancy-safe.

### The pattern it replaces A very common need is: 'get the value for this key; if there isn't one, create a sensible initial value, store it, and use it.' Written by hand: ```java List<Item> list = map.get(key); if (list == null) { list = new ArrayList<>(); map.put(key, list); } list.add(item); ``` Four lines, easy to get wrong. `computeIfAbsent` does it in one. ### What computeIfAbsent does, precisely `map.computeIfAbsent(key, mappingFunction)`: 1. If `key` is present with a **non-null** value, return that value; the function is **not** called. 2. Otherwise call `mappingFunction.apply(key)` (it receives the key). 3. If the function returns a **non-null** result, store `key -> result` and return it. 4. If the function returns **null**, leave the map unchanged and return null. The function is a `Function<K, V>` — it is **lazy** (runs only on a miss), which is why it is preferred over `putIfAbsent` when the default is expensive to build. ### The multimap / group-by idiom A 'multimap' is a map where each key holds a *collection* of values. With `computeIfAbsent`: ```java Map<String, List<String>> byFirstLetter = new HashMap<>(); for (String name : names) { String letter = name.substring(0, 1); byFirstLetter.computeIfAbsent(letter, k -> new ArrayList<>()).add(name); } ``` On the first occurrence of a letter, the empty list is created and stored; on later occurrences the **same** list is returned. The `.add(name)` then runs on that list. The empty list is created **exactly once** per key — no duplicate allocations, no null checks. ### Important caveats - **No self-modification:** the mapping function must not add or remove *other* keys of the same map. Doing so can throw `ConcurrentModificationException` or corrupt the map; the behavior is undefined. - **Concurrency:** on `ConcurrentHashMap`, computeIfAbsent runs the function atomically and may hold a lock on that hash bin while it runs. Keep the function fast and non-blocking, and never call another `computeIfAbsent` on the same map inside it (risk of deadlock/livelock). - **Null result = no entry:** returning null from the function creates nothing. - **Existing null:** like the other default methods, a key currently mapped to null is treated as absent, so the function runs. ### Why it beats getOrDefault here `getOrDefault(k, new ArrayList<>())` would allocate a **new list every call** and never store it, so the added items would be lost. `computeIfAbsent` allocates lazily and persists the value.

  • Why is computeIfAbsent(k, x -> new ArrayList<>()).add(v) correct but getOrDefault(k, new ArrayList<>()).add(v) buggy?
    computeIfAbsent stores the new list in the map and returns it, so subsequent adds hit the same list. getOrDefault returns a fresh, unstored list each time, so every added item is dropped.
  • What happens if the mapping function returns null?
    No mapping is created and computeIfAbsent returns null; the key stays absent.

saying these in an interview costs you the question

  • Using getOrDefault(k, new ArrayList<>()).add(v) (list is never stored)
  • Calling computeIfAbsent recursively on the same ConcurrentHashMap
  • Believing the function runs even when the key is present
  • Doing heavy/blocking work in the mapping function on a ConcurrentHashMap

context