Definition
- 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).
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"));
}
}
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.
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);
}
}
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.
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"));
}
}
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.
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());
}
}
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.
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);
}
}
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
// WRONG: loadFromDatabase runs EVERY time, even if key exists
map.putIfAbsent("SAV2001", loadFromDatabase("SAV2001"));
// CORRECT: function runs only when key is absent
map.computeIfAbsent("SAV2001", k -> loadFromDatabase(k));
Mistake: Using containsKey + put instead of merge
// WRONG: verbose, not atomic in ConcurrentHashMap
if (counts.containsKey(account)) {
counts.put(account, counts.get(account) + 1);
} else {
counts.put(account, 1);
}
// 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.
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);
}
}
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))andMap.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
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.
---