Back to Map Operations: compute, merge, putIfAbsent
Q1Question 1 of 2

What are computeIfAbsent, merge, putIfAbsent, getOrDefault?

Key Concepts
  • computeIfAbsent(key, mappingFunction) runs the function only when the key is absent, making it ideal for lazy initialisation and caching.
  • merge(key, value, remappingFunction) combines old and new values when the key exists, or inserts the value when absent, perfect for counting and accumulating.
  • putIfAbsent(key, value) inserts only when the key is missing and returns the existing value or null, but always evaluates the value eagerly.
  • getOrDefault(key, defaultValue) returns the mapped value or the default without modifying the map, replacing manual null checks.
  • compute(key, remappingFunction) runs the function regardless of key presence and removes the entry if the function returns null.
  • All five methods are atomic in ConcurrentHashMap, eliminating the race condition between containsKey and put.
  • These Java 8 methods replace the error-prone two-step containsKey-then-put pattern with single, intent-revealing operations.

Definition

Java 8 Map methods (computeIfAbsent, merge, putIfAbsent, getOrDefault, compute) replace the manual containsKey-then-put pattern with single atomic operations that are concurrency-safe in ConcurrentHashMap.
  • computeIfAbsent(key, function) – if the key is absent, call the function to compute the value, insert it, and return it; if present, return the existing value without calling the function
  • merge(key, value, remappingFunction) – if absent, insert the value; if present, apply the remapping function to the old and new values and replace
  • putIfAbsent(key, value) – insert the value only if the key is absent; return the existing value (or null if inserted)
  • getOrDefault(key, defaultValue) – return the mapped value if the key exists, otherwise return the default without inserting
  • compute(key, remappingFunction) – always run the function with the current value (or null if absent); if the function returns null, remove the entry

At National Bank of India, before Java 8, the clerk would check the filing cabinet (containsKey), walk back to the desk to prepare the folder (create value), then walk back to file it (put). Two trips. With computeIfAbsent, the clerk checks the cabinet and only prepares the folder if the slot is empty, in one trip. With merge, the clerk opens the existing folder, adds the new document to it, and files it back, all in one motion.

Understanding the Concepts

What are computeIfAbsent, merge, putIfAbsent, getOrDefault?

These are Java 8 default methods on the Map interface that combine lookup and mutation into a single operation. They replace the two-step pattern of checking for a key and then inserting or updating, which is both verbose and race-prone in concurrent code.

How does computeIfAbsent work?

computeIfAbsent calls the mapping function only when the key is absent. If the key exists, the function never runs. The return value is always the current value (existing or newly computed).

JAVA
package com.bank.demo;

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

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

        // Old way: two operations, verbose
        if (!tradesByAccount.containsKey("SAV2001")) {
            tradesByAccount.put("SAV2001", new ArrayList<>());
        }
        tradesByAccount.get("SAV2001").add("TRD-001");

        // New way: one operation, chainable
        tradesByAccount.computeIfAbsent("CUR3001", k -> new ArrayList<>()).add("TRD-002");
        tradesByAccount.computeIfAbsent("CUR3001", k -> new ArrayList<>()).add("TRD-003");

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

How does merge work?

merge is designed for combining values. If the key is absent, it inserts the value directly. If the key is present, it calls the remapping function with the old and new values and stores the result. If the function returns null, the entry is removed.

JAVA
package com.bank.demo;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

public class MergeDemo {
    public static void main(String[] args) {
        Map<String, BigDecimal> dailyTotals = new HashMap<>();

        // Counting transactions per account
        Map<String, Integer> txnCounts = new HashMap<>();
        String[] transactions = {"SAV2001", "CUR3001", "SAV2001", "SAV2001", "CUR3001"};

        for (String account : transactions) {
            txnCounts.merge(account, 1, Integer::sum);
        }
        System.out.println("Transaction counts: " + txnCounts);

        // Accumulating BigDecimal amounts
        BigDecimal[] amounts = {
            new BigDecimal("10000"), new BigDecimal("25000"),
            new BigDecimal("50000"), new BigDecimal("15000")
        };
        String[] accounts = {"SAV2001", "SAV2001", "CUR3001", "CUR3001"};

        for (int i = 0; i < accounts.length; i++) {
            dailyTotals.merge(accounts[i], amounts[i], BigDecimal::add);
        }
        System.out.println("Daily totals: " + dailyTotals);
    }
}
TEXT
Transaction counts: {SAV2001=3, CUR3001=2}
Daily totals: {SAV2001=35000, CUR3001=65000}

How does putIfAbsent work?

putIfAbsent inserts the value only when the key is absent. Unlike computeIfAbsent, the value is always evaluated (eager evaluation), even if the key already exists.

JAVA
package com.bank.demo;

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

public class PutIfAbsentDemo {
    public static void main(String[] args) {
        Map<String, String> accountOwners = new HashMap<>();
        accountOwners.put("SAV2001", "Rajesh");

        // Key exists: putIfAbsent does NOT overwrite, returns existing value
        String existing = accountOwners.putIfAbsent("SAV2001", "Priya");
        System.out.println("Existing: " + existing);
        System.out.println("Owner: " + accountOwners.get("SAV2001"));

        // Key absent: putIfAbsent inserts, returns null
        String inserted = accountOwners.putIfAbsent("CUR3001", "Priya");
        System.out.println("Inserted: " + inserted);
        System.out.println("Owner: " + accountOwners.get("CUR3001"));
    }
}
TEXT
Existing: Rajesh
Owner: Rajesh
Inserted: null
Owner: Priya

How does getOrDefault work?

getOrDefault returns the mapped value if the key exists, or the default value if the key is absent. It does not modify the map.

JAVA
package com.bank.demo;

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

public class GetOrDefaultDemo {
    public static void main(String[] args) {
        Map<String, Integer> loginAttempts = new HashMap<>();
        loginAttempts.put("SAV2001", 3);

        // Key exists: returns mapped value
        int savAttempts = loginAttempts.getOrDefault("SAV2001", 0);
        System.out.println("SAV2001 attempts: " + savAttempts);

        // Key absent: returns default, map unchanged
        int curAttempts = loginAttempts.getOrDefault("CUR3001", 0);
        System.out.println("CUR3001 attempts: " + curAttempts);
        System.out.println("Map size: " + loginAttempts.size());
    }
}
TEXT
SAV2001 attempts: 3
CUR3001 attempts: 0
Map size: 1

How does compute work?

compute always runs the remapping function, regardless of whether the key exists. If the key is absent, the old value passed to the function is null. If the function returns null, the entry is removed.

JAVA
package com.bank.demo;

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

public class ComputeDemo {
    public static void main(String[] args) {
        Map<String, Integer> failedLogins = new HashMap<>();

        // Key absent: old value is null
        failedLogins.compute("SAV2001", (k, v) -> v == null ? 1 : v + 1);
        System.out.println("After first: " + failedLogins);

        // Key present: old value is 1
        failedLogins.compute("SAV2001", (k, v) -> v == null ? 1 : v + 1);
        System.out.println("After second: " + failedLogins);

        // Return null to remove the entry
        failedLogins.compute("SAV2001", (k, v) -> null);
        System.out.println("After null return: " + failedLogins);
    }
}
TEXT
After first: {SAV2001=1}
After second: {SAV2001=2}
After null return: {}

How are these methods different from each other?

| Method | When function runs | Modifies map | Return value |
|---|---|---|---|
| computeIfAbsent | Only if key absent | Inserts if absent | Current value (existing or new) |
| compute | Always | Inserts, updates, or removes | New value (or null if removed) |
| merge | Only if key present (remapping) | Inserts or updates | New value |
| putIfAbsent | N/A (no function) | Inserts if absent | Existing value or null |
| getOrDefault | N/A (no function) | Never | Mapped value or default |

What are common mistakes?

Mistake: Using putIfAbsent for expensive values

JAVA
// WRONG: loadFromDatabase runs EVERY time, even if key exists
map.putIfAbsent("SAV2001", loadFromDatabase("SAV2001"));
JAVA
// CORRECT: function runs only when key is absent
map.computeIfAbsent("SAV2001", k -> loadFromDatabase(k));

Mistake: Using containsKey + put instead of merge

JAVA
// WRONG: verbose, not atomic in ConcurrentHashMap
if (counts.containsKey(account)) {
    counts.put(account, counts.get(account) + 1);
} else {
    counts.put(account, 1);
}
JAVA
// CORRECT: single atomic operation
counts.merge(account, 1, Integer::sum);

What about thread safety?

In ConcurrentHashMap, all five methods are atomic. The function is executed while holding the bucket lock, ensuring that no other thread can interleave a check-then-act sequence. In a plain HashMap, these methods are not thread-safe.

JAVA
package com.bank.demo;

import java.util.concurrent.ConcurrentHashMap;
import java.util.List;
import java.util.ArrayList;

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

        // Thread-safe lazy initialisation
        cache.computeIfAbsent("SAV2001", k -> new ArrayList<>()).add("TRD-001");
        // Even with 100 concurrent threads, the list is created exactly once.

        System.out.println("Cache: " + cache);
    }
}
TEXT
Cache: {SAV2001=[TRD-001]}

What should a senior engineer know?

  • computeIfAbsent in ConcurrentHashMap blocks the bucket. If the mapping function is slow (for example, a network call), other threads waiting for the same bucket are blocked. Keep the function fast or use a different caching strategy.
  • merge with null return removes the entry. This is intentional and useful for decrement-to-zero patterns: counts.merge(key, -1, Integer::sum) followed by removal when the count reaches zero.
  • replaceAll and forEach. Java 8 also added Map.replaceAll((k, v) -> transform(v)) and Map.forEach((k, v) -> process(k, v)) for bulk operations.
  • computeIfPresent. Runs the function only when the key exists. If the function returns null, the entry is removed.

What are common interview follow-up questions?

  • "What is the difference between putIfAbsent and computeIfAbsent?" : putIfAbsent eagerly evaluates the value; computeIfAbsent lazily evaluates via a function.
  • "Is merge atomic in ConcurrentHashMap?" : Yes. The remapping function runs under the bucket lock.
  • "What happens if the function in computeIfAbsent returns null?" : No mapping is recorded. The method returns null.
  • "When would you use compute instead of merge?" : When you need access to the key inside the function, or when you want to handle both absent and present cases in one function.

Visual Overview

DIAGRAM
  Java 8 Map Method Decision Flow
  ================================

  Need to read a value with a fallback?
  └── getOrDefault(key, default)
      (does NOT modify the map)

  Need to insert only if absent?
  ├── Value is cheap to create?
  │   └── putIfAbsent(key, value)         [eager]
  └── Value is expensive (DB, network)?
      └── computeIfAbsent(key, k -> ...)   [lazy]

  Need to combine old + new value?
  └── merge(key, newVal, (old, new) -> ...) [counting, summing]

  Need full control (insert, update, or remove)?
  └── compute(key, (k, oldVal) -> ...)
      (null return removes entry)

  ConcurrentHashMap:
  All methods above are ATOMIC (bucket-locked)
  ┌────────────────────────────────────────────┐
  │ Old: containsKey + get + put = 3 ops       │
  │      Race condition between ops!            │
  │                                             │
  │ New: computeIfAbsent = 1 atomic op          │
  │      No race condition                      │
  └────────────────────────────────────────────┘

Key Takeaway

Java 8 introduced five Map methods that replace the verbose and race-prone containsKey-then-put pattern. computeIfAbsent is for lazy initialisation and caching: the function runs only when the key is absent. merge is for combining values: counting with Integer::sum or accumulating with BigDecimal::add. putIfAbsent is for simple insert-if-missing with an eagerly constructed value. getOrDefault is for null-safe reads without modifying the map. compute is for full control over both absent and present cases.

The critical advantage of these methods is atomicity in ConcurrentHashMap. The old two-step pattern (check, then put) has a race window where another thread can intervene. The new methods execute the check and mutation as a single atomic operation under the bucket lock.

In the National Bank of India's transaction processing system, Rajesh replaced all manual containsKey-then-put patterns with these methods. Transaction counting became counts.merge(account, 1, Integer::sum). Trade grouping became trades.computeIfAbsent(account, k -> new ArrayList<>()).add(trade). The code became shorter, clearer, and concurrency-safe.

Remember one sentence: use computeIfAbsent for lazy caching, merge for counting and accumulating, and avoid the manual containsKey-then-put pattern because it is verbose and not atomic.

---

Share
Use ← → arrow keys to navigate