Java Map computeIfAbsent(): Multimaps, Caches and null Rules

Learn how Java Map computeIfAbsent() works, with multimap and cache examples, null and exception rules, thread safety and the putIfAbsent() difference.

Flow chart of Map computeIfAbsent showing when the mapping function runs and what gets stored

The Map.computeIfAbsent() method returns the value mapped to a key, and when the key is missing or mapped to null, it first calls a mapping function to create the value and stores it. The function runs only for missing keys, so the value is computed once and reused on every later call.

We use computeIfAbsent() for two everyday jobs. The first is a multimap, where each key holds a list or a set that is created on first use. The second is a cache, where a slow lookup runs only the first time a key is requested.

The following example builds a map of blog posts per tag and a small cache of squares, with the result of each call as a comment.

Map<String, List<String>> postsByTag = new HashMap<>();
postsByTag.computeIfAbsent("java", tag -> new ArrayList<>()).add("Records");
postsByTag.computeIfAbsent("java", tag -> new ArrayList<>()).add("Streams");
List<String> javaPosts = postsByTag.get("java");                 // [Records, Streams]

Map<Integer, Integer> squares = new HashMap<>(Map.of(3, 9));
Integer cached = squares.computeIfAbsent(3, n -> n * n);          // 9 (function not called)
Integer computed = squares.computeIfAbsent(4, n -> n * n);        // 16 (computed and stored)
Integer nothing = squares.computeIfAbsent(5, n -> null);          // null (nothing stored)

Notice that the second computeIfAbsent(“java”, …) call returns the list created by the first call, so both titles end up in one list. We look at the two main use cases first, go through the exact rules for null values and exceptions, and compare the method with putIfAbsent(), compute(), computeIfPresent() and merge().

1. When to Use the computeIfAbsent() Method?

The method was added to the Map interface in Java 8 as a default method, and the main implementations such as HashMap, TreeMap and ConcurrentHashMap override it with faster code. It replaces the old three-step pattern of get(), a null check and put() with one call.

1.1. When the Value Is Expensive to Compute

A currency converter in a shop backend gets exchange rates from a remote API. Each call takes a few hundred milliseconds, and the same currency code is requested thousands of times a day. With computeIfAbsent(), the remote call runs once per currency, and every later request reads the stored rate.

The following example counts the calls to the loading function, which stands in for the remote API.

AtomicInteger calls = new AtomicInteger();
Function<String, Double> loadRate = code -> {
    calls.incrementAndGet();                                     // remote call in a real app
    return code.equals("USD") ? 1.08 : 0.86;
};

Map<String, Double> rates = new HashMap<>();
double first = rates.computeIfAbsent("USD", loadRate);           // 1.08 (loaded)
double second = rates.computeIfAbsent("USD", loadRate);          // 1.08 (from the map)
double pound = rates.computeIfAbsent("GBP", loadRate);           // 0.86 (loaded)
int remoteCalls = calls.get();                                   // 2

The map in the example grows without limit and never refreshes a rate. For a real cache with expiry and a size limit, a library such as Caffeine is the better choice, and it uses the same idea of a loading function per key.

1.2. Creating a Multimap

A multimap maps one key to several values, which we store in a List or a Set per key. The first value of each key needs special handling, because the list does not exist yet. The computeIfAbsent() call creates the list for a new key and returns the existing list for a known key, so the add() call always has a list to work on.

List<String> titles = List.of("Records", "Streams", "Sealed classes", "Spring Boot");
Map<Character, List<String>> byLetter = new TreeMap<>();
for (String title : titles) {
    byLetter.computeIfAbsent(title.charAt(0), letter -> new ArrayList<>()).add(title);
}
Map<Character, List<String>> index = byLetter;   // {R=[Records], S=[Streams, Sealed classes, Spring Boot]}

When the values come from a collection that we already have, Collectors.groupingBy() builds the same multimap in one stream expression. The computeIfAbsent() version is the better fit when values arrive one at a time, for example from a message listener or a loop that reads a file.

List<String> titles = List.of("Records", "Streams", "Sealed classes", "Spring Boot");
Map<Character, List<String>> byLetter = titles.stream().collect(Collectors.groupingBy(t -> t.charAt(0), TreeMap::new, Collectors.toList()));   // {R=[Records], S=[Streams, Sealed classes, Spring Boot]}

For counters, merge() is shorter than computeIfAbsent(), because merge(word, 1, Integer::sum) adds 1 to an existing count or stores 1 for a new word.

2. Map.computeIfAbsent() Syntax

The method takes the key and a Function that receives the key and returns the new value, and its full signature is default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction). The function never sees the old value, because it runs only when there is no usable old value.

Flow chart of Map computeIfAbsent showing when the mapping function runs and what gets stored
The mapping function runs only when the key is missing or mapped to null, and a null result stores nothing

Each parameter has one rule, and the return value depends on what the map held before the call.

  • The key parameter is the key to look up, and it is also passed to the function.
  • The mappingFunction parameter computes the value for a missing key. It must not be null.
  • The return value is the current value of the key after the call. It is either the existing value, the newly computed value, or null when the function returned null.

Each case of the call has a defined outcome, and the difference between a missing key and a key mapped to null matters in a HashMap, which allows null values.

Map state before the callFunction called?What the map holds after the callReturn value
Key mapped to a non-null valueNoUnchangedThe existing value
Key missingYesNew entry, unless the function returns nullThe computed value
Key mapped to nullYesThe null is replaced, unless the function returns nullThe computed value
Function returns nullYesNo new entry; an existing null entry staysnull
Function throws an exceptionYesUnchangedNone, the exception propagates

3. Handling null and Exceptions in the Mapping Function

Most problems with computeIfAbsent() come from the mapping function, not from the map. These rules hold for HashMap, TreeMap and LinkedHashMap on Java 25, and the last one is specific to how the JDK maps detect changes.

3.1. The Mapping Function Must Not Be null

Passing null instead of a function throws a NullPointerException, even when the key already exists and the function would never run.

Map<String, Integer> stock = new HashMap<>(Map.of("apple", 5));
Integer value = stock.computeIfAbsent("apple", null);            // NullPointerException

3.2. The Mapping Function May Return null

When the function returns null, the method records no mapping and returns null. A key that was already mapped to null keeps its null entry, so containsKey() still returns true for it. A null result does not remove anything, which is the main difference from compute().

Map<String, Integer> stock = new HashMap<>();
Integer none = stock.computeIfAbsent("kiwi", k -> null);         // null
boolean stored = stock.containsKey("kiwi");                       // false

stock.put("plum", null);
Integer replaced = stock.computeIfAbsent("plum", k -> 7);        // 7 (null value counts as absent)

A null result becomes a problem when we assign it to a primitive variable, because unboxing null throws a NullPointerException. So we keep the result in a wrapper type such as Integer, or make sure the function never returns null.

Map<String, Integer> stock = new HashMap<>();
int unboxed = stock.computeIfAbsent("fig", k -> null);           // NullPointerException
Integer safe = stock.computeIfAbsent("fig", k -> null);          // null

3.3. The Mapping Function Throws an Exception

If the function throws an unchecked exception, computeIfAbsent() rethrows it and leaves the map unchanged. The next call for the same key runs the function again, which is the behavior we want for a remote call that failed once.

Map<String, Integer> stock = new HashMap<>();
Integer failed = stock.computeIfAbsent("kiwi", k -> Integer.parseInt("n/a"));   // NumberFormatException
boolean present = stock.containsKey("kiwi");                                     // false

A Function cannot throw checked exceptions. When the loading code throws an IOException, we catch it inside the lambda and wrap it in an UncheckedIOException.

3.4. The Mapping Function Must Not Change the Map

The mapping function must not add or remove entries in the same map, and on Java 25, HashMap and TreeMap throw a ConcurrentModificationException when they detect such a change. The HashMap check exists since Java 9, and it works on a best-effort basis. The classic case is a recursive cache, such as Fibonacci numbers, where the function calls computeIfAbsent() again for smaller numbers.

static long fib(int n, Map<Integer, Long> cache) {
    if (n < 2) {
        return n;
    }
    return cache.computeIfAbsent(n, k -> fib(k - 1, cache) + fib(k - 2, cache));
}
long broken = fib(30, new HashMap<>());                          // ConcurrentModificationException

The fix is to compute the value outside of computeIfAbsent() and store it with put(). The recursive calls finish before the put(), so the map is never changed during another update.

static long fibSafe(int n, Map<Integer, Long> cache) {
    if (n < 2) {
        return n;
    }
    Long known = cache.get(n);
    if (known != null) {
        return known;
    }
    long value = fibSafe(n - 1, cache) + fibSafe(n - 2, cache);
    cache.put(n, value);
    return value;
}
long fifty = fibSafe(50, new HashMap<>());                       // 12586269025

4. Difference With putIfAbsent(), compute() and computeIfPresent()

The Map interface has five methods that look alike, and each one answers a different question. The two things to check are when the function runs and what a null result does.

MethodRuns whenFunction inputnull resultTypical use
computeIfAbsent(k, f)Key missing or mapped to nullKeyNothing storedCache, multimap
putIfAbsent(k, v)Always evaluates v firstNone, a ready valueStores null for a missing keyDefault value that is cheap to create
computeIfPresent(k, f)Key mapped to a non-null valueKey and old valueRemoves the keyUpdate or remove existing entries
compute(k, f)AlwaysKey and old value (may be null)Removes the keyCreate or update in one call
merge(k, v, f)Key mapped to a non-null value, otherwise v is storedOld value and vRemoves the keyCounters, joining values

4.1. computeIfAbsent() vs putIfAbsent()

The putIfAbsent() method takes a ready value, so Java creates that value on every call, even when the key exists. It also returns the previous value, which is null for a new key, so chaining add() on it fails the first time.

Map<String, List<String>> postsByTag = new HashMap<>();
boolean added = postsByTag.putIfAbsent("java", new ArrayList<>()).add("Records");   // NullPointerException
Map<String, List<String>> postsByTag = new HashMap<>();
boolean added = postsByTag.computeIfAbsent("java", tag -> new ArrayList<>()).add("Records");   // true
List<String> posts = postsByTag.get("java");                                                 // [Records]

4.2. compute() and computeIfPresent()

Both methods take a BiFunction that receives the key and the current value. The computeIfPresent() method skips keys mapped to null, whereas compute() always runs and passes null as the old value. A null result from either of them removes the entry.

Map<String, Integer> visits = new HashMap<>();
visits.put("home", null);
Integer skipped = visits.computeIfPresent("home", (k, v) -> v + 1);           // null (not called)
Integer firstVisit = visits.compute("home", (k, v) -> v == null ? 1 : v + 1);  // 1
Integer nextVisit = visits.compute("home", (k, v) -> v == null ? 1 : v + 1);   // 2
Integer removed = visits.computeIfPresent("home", (k, v) -> null);            // null (entry removed)
boolean exists = visits.containsKey("home");                                   // false

5. Is computeIfAbsent() Thread-Safe?

The answer depends on the map. A plain HashMap has no synchronization, so two threads calling computeIfAbsent() for the same key can both run the function, and one result is lost. A ConcurrentHashMap performs the whole call atomically, and the function runs at most once per missing key.

A web app that counts page hits per URL from many request threads is a good fit. The computeIfAbsent() call creates one AtomicInteger per URL, and every thread increments the same counter.

ConcurrentMap<String, AtomicInteger> hits = new ConcurrentHashMap<>();
try (ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 1000; i++) {
        pool.submit(() -> hits.computeIfAbsent("/home", url -> new AtomicInteger()).incrementAndGet());
    }
}
int total = hits.get("/home").get();                             // 1000

Other threads that update the same key wait while the function runs, so the function in a ConcurrentMap has to be short. A slow remote call inside it blocks every request for that key and sometimes for other keys in the same bucket too. The ConcurrentHashMap best practices cover that trade-off in more detail.

6. computeIfAbsent() FAQs

Developers who start using computeIfAbsent() often search for these follow-up questions.

6.1. Is the Function Called if the Key Is Mapped to null?

Yes. A key mapped to null counts as absent, so the function runs and its result replaces the null. If the function returns null again, the null entry stays as it is.

6.2. Does computeIfAbsent() Return the Old or the New Value?

The method returns the value that is in the map after the call. For an existing key, that is the old value. For a missing key, it is the new value, or null when the function returned null. This is different from put() and putIfAbsent(), which return the previous value.

6.3. Should I Use computeIfAbsent() or getOrDefault()?

Use getOrDefault() to read a fallback value without changing the map, and computeIfAbsent() when the value must be stored for later calls.

Map<String, Integer> limits = new HashMap<>();
int readOnly = limits.getOrDefault("guest", 10);                 // 10 (map unchanged)
int stored = limits.computeIfAbsent("guest", k -> 10);           // 10 (map now has guest=10)
int size = limits.size();                                        // 1

6.4. Can computeIfAbsent() Cause a ConcurrentModificationException?

Yes, when the mapping function changes the same HashMap or TreeMap, for example in a recursive cache. As we saw in section 3.4, the fix is to compute the value first and call put() after it.

7. Conclusion

The computeIfAbsent() method looks up a key and, only when the key is missing or mapped to null, runs a function to create and store the value. It turns the get(), check and put() sequence into one call, which is why it is the standard way to build multimaps and simple caches.

A null result stores nothing and removes nothing, an exception leaves the map unchanged, and the function must never modify the map it belongs to. For shared maps, ConcurrentHashMap runs the function at most once per key, so we keep that function short.

8. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.