Back to Map Operations: compute, merge, putIfAbsent
Q2Question 2 of 2

What is putIfAbsent vs computeIfAbsent?

Key Concepts
  • putIfAbsent(key, value) evaluates the value eagerly: the value object is constructed before the method checks whether the key exists.
  • computeIfAbsent(key, function) evaluates the function lazily: it runs only when the key is absent, saving the cost of unnecessary creation.
  • For expensive operations like database queries or network calls, computeIfAbsent avoids wasted work when the key already exists.
  • putIfAbsent returns the existing value if present or null if it inserted; computeIfAbsent returns the existing or newly computed value (never null if the function returns non-null).
  • computeIfAbsent is chainable: map.computeIfAbsent(key, k -> new ArrayList<>()).add(item) works in one line.
  • Both are atomic in ConcurrentHashMap, but computeIfAbsent holds the bucket lock while the function runs, so keep the function fast.
  • Use computeIfAbsent as the standard cache pattern and putIfAbsent only when the value is trivially cheap to construct.

Definition

putIfAbsent always constructs the value before checking the key (eager), while computeIfAbsent runs the mapping function only when the key is absent (lazy), making computeIfAbsent the right choice for expensive value creation.
  • putIfAbsent(key, value) – the value parameter is evaluated before the method call; if the key already exists, the pre-built value is discarded
  • computeIfAbsent(key, function) – the function parameter is a lambda that runs only when the key is absent; if the key exists, the function is never called
  • Return values differ – putIfAbsent returns the existing value (or null if it inserted); computeIfAbsent returns the current value (existing or newly computed)
  • Chainability – computeIfAbsent returns the value directly, enabling .add() or .put() chaining; putIfAbsent requires a separate get() call
  • Atomicity – both are atomic in ConcurrentHashMap; computeIfAbsent's function runs under the bucket lock

At National Bank of India, putIfAbsent is like Rajesh filling out a complete loan application form (expensive) before checking whether the customer already has one on file. If the customer already applied, the form is thrown away. computeIfAbsent is like Rajesh checking the filing cabinet first: only if no application exists does he fill out the form. For a simple post-it note (cheap value), the wasted effort is negligible. For a full loan application (database query), the wasted effort is expensive.

Understanding the Concepts

What is the difference between putIfAbsent and computeIfAbsent?

The core difference is evaluation strategy. putIfAbsent uses eager evaluation: the value is fully constructed before the method runs. computeIfAbsent uses lazy evaluation: the mapping function runs only when the key is absent.

JAVA
package com.bank.demo;

import java.util.HashMap;
import java.util.Map;

public class EagerVsLazyDemo {
    private static int constructionCount = 0;

    private static String expensiveCreate(String key) {
        constructionCount++;
        System.out.println("  Constructing value for: " + key);
        return "data-for-" + key;
    }

    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();

        System.out.println("--- putIfAbsent (eager) ---");
        constructionCount = 0;
        map.putIfAbsent("SAV2001", expensiveCreate("SAV2001")); // call 1: constructs + inserts
        map.putIfAbsent("SAV2001", expensiveCreate("SAV2001")); // call 2: constructs but discards!
        map.putIfAbsent("SAV2001", expensiveCreate("SAV2001")); // call 3: constructs but discards!
        System.out.println("Constructions: " + constructionCount);

        map.clear();

        System.out.println("\n--- computeIfAbsent (lazy) ---");
        constructionCount = 0;
        map.computeIfAbsent("SAV2001", k -> expensiveCreate(k)); // call 1: constructs + inserts
        map.computeIfAbsent("SAV2001", k -> expensiveCreate(k)); // call 2: key exists, no call!
        map.computeIfAbsent("SAV2001", k -> expensiveCreate(k)); // call 3: key exists, no call!
        System.out.println("Constructions: " + constructionCount);
    }
}
TEXT
--- putIfAbsent (eager) ---
  Constructing value for: SAV2001
  Constructing value for: SAV2001
  Constructing value for: SAV2001
Constructions: 3

--- computeIfAbsent (lazy) ---
  Constructing value for: SAV2001
Constructions: 1

Why does this matter for performance?

When the value is cheap to create (a simple string, a small integer), the difference is negligible. When the value requires an expensive operation (database query, network call, complex computation), putIfAbsent wastes significant resources.

JAVA
package com.bank.demo;

import java.math.BigDecimal;
import java.util.concurrent.ConcurrentHashMap;

public class CachePatternDemo {

    // Simulates an expensive database lookup
    private static BigDecimal loadBalanceFromDB(String accountId) {
        System.out.println("  DB query for: " + accountId);
        // In production: JDBC call to Oracle, 5-50ms
        return new BigDecimal("500000.00");
    }

    public static void main(String[] args) {
        ConcurrentHashMap<String, BigDecimal> balanceCache = new ConcurrentHashMap<>();

        // WRONG: putIfAbsent runs the DB query every time
        System.out.println("--- putIfAbsent (wasteful) ---");
        balanceCache.putIfAbsent("SAV2001", loadBalanceFromDB("SAV2001"));
        balanceCache.putIfAbsent("SAV2001", loadBalanceFromDB("SAV2001"));
        balanceCache.putIfAbsent("SAV2001", loadBalanceFromDB("SAV2001"));

        balanceCache.clear();

        // CORRECT: computeIfAbsent runs the DB query only once
        System.out.println("\n--- computeIfAbsent (efficient) ---");
        balanceCache.computeIfAbsent("SAV2001", k -> loadBalanceFromDB(k));
        balanceCache.computeIfAbsent("SAV2001", k -> loadBalanceFromDB(k));
        balanceCache.computeIfAbsent("SAV2001", k -> loadBalanceFromDB(k));

        System.out.println("\nCached balance: " + balanceCache.get("SAV2001"));
    }
}
TEXT
--- putIfAbsent (wasteful) ---
  DB query for: SAV2001
  DB query for: SAV2001
  DB query for: SAV2001

--- computeIfAbsent (efficient) ---
  DB query for: SAV2001

Cached balance: 500000.00

How do the return values differ?

The return value semantics are different and affect how you chain operations.

| Aspect | putIfAbsent | computeIfAbsent |
|---|---|---|
| Key absent | Returns null, inserts value | Returns computed value |
| Key present | Returns existing value, no change | Returns existing value, no change |
| Chainable | No (returns null on insert) | Yes (always returns a value) |

JAVA
package com.bank.demo;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ReturnValueDemo {
    public static void main(String[] args) {
        Map<String, List<String>> trades = new HashMap<>();

        // putIfAbsent: two steps needed
        trades.putIfAbsent("SAV2001", new ArrayList<>());
        trades.get("SAV2001").add("TRD-001");

        // computeIfAbsent: one step, chainable
        trades.computeIfAbsent("CUR3001", k -> new ArrayList<>()).add("TRD-002");

        System.out.println("SAV2001: " + trades.get("SAV2001"));
        System.out.println("CUR3001: " + trades.get("CUR3001"));
    }
}
TEXT
SAV2001: [TRD-001]
CUR3001: [TRD-002]

What are common mistakes?

Mistake: Using putIfAbsent for database-backed caches

JAVA
// WRONG: DB query runs every time, even on cache hit
cache.putIfAbsent("SAV2001", accountRepo.findById("SAV2001"));
// 100 calls = 100 DB queries, only 1 useful
JAVA
// CORRECT: DB query runs only on cache miss
cache.computeIfAbsent("SAV2001", k -> accountRepo.findById(k));
// 100 calls = 1 DB query + 99 cache hits

Mistake: Assuming putIfAbsent is chainable

JAVA
// WRONG: putIfAbsent returns null on first insert, causes NPE
map.putIfAbsent("SAV2001", new ArrayList<>()).add("TRD-001"); // NPE!
JAVA
// CORRECT: computeIfAbsent always returns the value
map.computeIfAbsent("SAV2001", k -> new ArrayList<>()).add("TRD-001");

What about thread safety and concurrency?

Both methods are atomic in ConcurrentHashMap. However, computeIfAbsent holds the bucket lock while the mapping function executes. If the function performs a slow operation (network I/O, blocking call), other threads waiting for the same bucket are blocked.

JAVA
package com.bank.demo;

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentAtomicityDemo {
    public static void main(String[] args) {
        ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();

        // Atomic: even with 50 threads, the function runs exactly once per key
        cache.computeIfAbsent("SAV2001", k -> {
            System.out.println("Computing on thread: " + Thread.currentThread().getName());
            return "Rajesh";
        });

        System.out.println("Result: " + cache.get("SAV2001"));
    }
}
TEXT
Computing on thread: main
Result: Rajesh

For slow mapping functions, consider computing the value outside the map and then using putIfAbsent, or use a dedicated cache library like Caffeine that handles concurrent loading with futures.

When should you use putIfAbsent?

Use putIfAbsent when the value is trivially cheap to create and you do not need the return value for chaining. For example, setting a default string: map.putIfAbsent("status", "ACTIVE").

When should you use computeIfAbsent?

Use computeIfAbsent for caches, grouping operations, lazy initialisation, and any scenario where the value is expensive to create or where you need to chain the return value.

What should a senior engineer know?

  • ConcurrentHashMap deadlock risk. If the computeIfAbsent mapping function recursively calls computeIfAbsent on the same map with a key that hashes to the same bucket, a deadlock can occur (bucket lock re-entrance). This is documented in the JDK.
  • Null value semantics. If the mapping function returns null, computeIfAbsent does not insert an entry and returns null. putIfAbsent can insert a null value (the key maps to null).
  • Java 9 improvement. ConcurrentHashMap.computeIfAbsent was optimised in JDK 9 to avoid unnecessary synchronisation when the key is present.
  • Caffeine and Guava. For production caches with TTL, max size, and refresh-ahead, use Caffeine (Caffeine.newBuilder().maximumSize(10000).build(loader)) rather than a raw ConcurrentHashMap.

What are common interview follow-up questions?

  • "When would you use putIfAbsent over computeIfAbsent?" : When the value is trivially cheap and you do not need chaining.
  • "What happens if the mapping function throws an exception?" : No entry is inserted. The exception propagates to the caller.
  • "Can the mapping function modify the map?" : It should not. In ConcurrentHashMap, doing so risks deadlock. The Javadoc explicitly warns against this.
  • "How many times does the DB query run with putIfAbsent vs computeIfAbsent for 100 calls?" : putIfAbsent runs it 100 times; computeIfAbsent runs it once.

Visual Overview

DIAGRAM
  putIfAbsent vs computeIfAbsent
  ==============================

  putIfAbsent(key, value):
  ┌──────────────────────────────────────┐
  │ 1. Evaluate value (ALWAYS runs)      │
  │ 2. Check: key in map?                │
  │    YES -> discard value, return old  │
  │    NO  -> insert value, return null  │
  └──────────────────────────────────────┘

  computeIfAbsent(key, function):
  ┌──────────────────────────────────────────┐
  │ 1. Check: key in map?                    │
  │    YES -> return existing (function SKIP)│
  │    NO  -> run function (LAZY)            │
  │           insert result, return it       │
  └──────────────────────────────────────────┘

  100 calls for same key:
  ┌──────────────────────────────┐
  │ putIfAbsent:                 │
  │   value created: 100 times   │
  │   value used:    1 time      │
  │   wasted:       99 objects   │
  │                              │
  │ computeIfAbsent:             │
  │   function called: 1 time    │
  │   value used:      1 time    │
  │   wasted:          0 objects │
  └──────────────────────────────┘

Key Takeaway

The difference between putIfAbsent and computeIfAbsent is evaluation strategy. putIfAbsent is eager: the value is fully constructed before the method checks the map. computeIfAbsent is lazy: the mapping function runs only when the key is absent. For expensive operations like database queries, network calls, or complex object creation, this difference is enormous.

computeIfAbsent is also more ergonomic. It returns the current value (existing or new), enabling one-liner chains like map.computeIfAbsent(key, k -> new ArrayList<>()).add(item). putIfAbsent returns null on insertion, requiring a separate get() call. This makes computeIfAbsent the standard pattern for caches and grouping operations.

In the National Bank of India's account balance cache, Rajesh replaced all putIfAbsent calls with computeIfAbsent. The cache served 50,000 lookups per second. With putIfAbsent, each lookup triggered a database query regardless of cache state. With computeIfAbsent, only the first lookup per account hit the database, reducing DB load by two orders of magnitude.

Remember one sentence: use computeIfAbsent for caches and lazy initialisation because the function runs only when the key is absent, unlike putIfAbsent which always constructs the value.

---

Share
Use ← → arrow keys to navigate