After a Decade with Map, Are You Still Using containsKey + get + put?
The article reviews the seven Java 8 Map convenience APIs—getOrDefault, putIfAbsent, computeIfAbsent, computeIfPresent, compute, merge, and forEach—plus replaceAll and Map.of, showing concise code examples, best‑practice recommendations, concurrency considerations, and common pitfalls for modern Java developers.
Why the old pattern is redundant
Long‑time Java developers often write the verbose
if (map.containsKey(key)) { map.put(key, map.get(key) + 1); } else { map.put(key, 1); }pattern. Java 8 introduced a suite of "efficiency APIs" that replace this boilerplate with a single line, e.g. map.merge(key, 1, Integer::sum);.
1. getOrDefault – avoid manual null checks
// old
String name = map.containsKey(id) ? map.get(id) : "unknown";
// new
String name = map.getOrDefault(id, "unknown");Returns the value associated with id or the supplied default, making dictionary look‑ups and configuration reads concise.
2. putIfAbsent – insert only when missing
// old
if (!map.containsKey(key)) {
map.put(key, value);
}
// new
map.putIfAbsent(key, value);The method returns the previous value (or null if the insertion succeeded).
3. computeIfAbsent – lazy loading and caching
// old
List<User> users = cache.get(dept);
if (users == null) {
users = userService.findByDept(dept);
cache.put(dept, users);
}
users.add(newUser);
// new
List<User> users = cache.computeIfAbsent(dept, k -> userService.findByDept(k));
users.add(newUser);Computes the value only when the key is absent, then stores and returns it. Ideal for local caches, lazy initialization, and per‑key collection accumulation.
4. computeIfPresent – update only when present
map.computeIfPresent(key, (k, v) -> v + 1);If the key exists, the callback calculates a new value; if the callback returns null, the entry is removed.
5. compute – universal updater
map.compute(key, (k, v) -> v == null ? 1 : v + 1);Combines the logic of computeIfAbsent and computeIfPresent. Returning null deletes the entry.
6. merge – aggregation shortcut
// word‑frequency example
Map<String, Integer> count = new HashMap<>();
for (String word : words) {
count.merge(word, 1, Integer::sum);
}When the key is absent, it inserts (key, value); when present, it merges the old and new values via the supplied BiFunction. Suitable for counters, string concatenation, and collection merging.
Do not pass null as the second argument to merge ; the Map contract will throw a NullPointerException .
7. forEach – streamlined iteration
// old
for (Map.Entry<String, Integer> e : map.entrySet()) {
System.out.println(e.getKey() + "=" + e.getValue());
}
// new
map.forEach((k, v) -> System.out.println(k + "=" + v));Saves several lines and improves readability.
Additional APIs
replaceAll – bulk value transformation
map.replaceAll((k, v) -> v.toUpperCase());Updates every value in place, e.g., converting all strings to upper case.
Map.of / Map.ofEntries – immutable map literals (Java 9+)
Map<String, Integer> m = Map.of("a", 1, "b", 2, "c", 3);
// for more than 10 entries
Map<String, Integer> big = Map.ofEntries(
Map.entry("a", 1),
Map.entry("b", 2),
Map.entry("c", 3)
);Creates read‑only maps; they reject null keys/values and throw on duplicate keys.
Real‑world scenarios
Scenario 1 – cache with lazy loading
private final Map<Long, User> userCache = new ConcurrentHashMap<>();
public User getUser(Long id) {
return userCache.computeIfAbsent(id, userService::findById);
} ConcurrentHashMap.computeIfAbsentis atomic, ensuring the value is computed only once per key.
If the callback returns null , nothing is stored, so subsequent calls will recompute. To cache a "not‑found" result, store a placeholder object or Optional .
Cache penetration : Repeated look‑ups for missing data cause repeated DB hits; cache the absence.
Recursive update : Updating the same key inside a ConcurrentHashMap callback throws IllegalStateException (Java 9+).
Scenario 2 – word‑frequency counting
Map<String, Long> freq = new HashMap<>();
words.forEach(w -> freq.merge(w, 1L, Long::sum));Scenario 3 – grouping by a field
Map<Department, List<Employee>> byDept = new HashMap<>();
employees.forEach(e ->
byDept.computeIfAbsent(e.getDept(), k -> new ArrayList<>()).add(e)
);While Collectors.groupingBy offers a stream‑based alternative, the explicit map approach is sometimes clearer when iterating and inserting simultaneously.
Scenario 4 – safe numeric accumulation
balanceMap.merge(userId, amount, BigDecimal::add);Uses BigDecimal::add for monetary sums and Integer::sum for integer counters.
Scenario 5 – conditional update
sessionMap.computeIfPresent(token, (k, s) -> {
s.setLastActive(now());
return s;
});Updates the session's last‑active timestamp only when the token exists, avoiding unnecessary put calls.
Common pitfalls
Thread safety : The new APIs are not atomic on a plain HashMap. Use ConcurrentHashMap for concurrent scenarios.
Modifying the map inside callbacks : Doing map.put(...) or another compute on the same map can trigger ConcurrentModificationException or, in Java 9+, IllegalStateException: Recursive update.
Conclusion
getOrDefault – fetch with a default.
putIfAbsent – insert only when missing.
computeIfAbsent – lazy load or per‑key accumulation.
computeIfPresent – modify only when present.
compute – universal updater.
merge – aggregation shortcut.
forEach – concise iteration.
replaceAll and Map.of – bulk updates and immutable map creation.
Use ConcurrentHashMap for thread‑safe scenarios, remembering cache‑penetration and recursive‑update hazards.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
