Collections and Map Enhancements: forEach, merge, compute, replaceAll
Overview
Java 8 didn’t just add Streams — it also enhanced the existing Iterable, Collection, List, and Map interfaces with default methods that cover the most common imperative patterns. Understanding these methods lets you write cleaner code even without streams.
Iterable.forEach
forEach takes a Consumer<T> and applies it to every element:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// Before Java 8
for (String name : names) {
System.out.println(name);
}
// Java 8
names.forEach(System.out::println);
names.forEach(name -> System.out.println("Hello, " + name));
Works on any Iterable — lists, sets, queues, and custom collections:
Set<Integer> nums = new HashSet<>(Arrays.asList(1, 2, 3));
nums.forEach(n -> System.out.println(n * 2));
Limitation: You cannot use break or continue inside forEach. For those patterns, use a traditional loop or a stream with findFirst / anyMatch.
Collection.removeIf
Removes all elements matching a predicate in one call — no iterator boilerplate:
List<String> names = new ArrayList<>(Arrays.asList("Alice", "", "Bob", " ", "Charlie"));
// Before Java 8 — the safe way to remove during iteration
Iterator<String> it = names.iterator();
while (it.hasNext()) {
if (it.next().trim().isEmpty()) it.remove();
}
// Java 8
names.removeIf(s -> s.trim().isEmpty());
More examples:
// Remove expired sessions
sessions.removeIf(Session::isExpired);
// Remove elements matching criteria from a set
Set<Integer> numbers = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5, 6));
numbers.removeIf(n -> n % 2 == 0); // removes evens: {1, 3, 5}
removeIf modifies the collection in place. It works on any Collection — lists, sets, queues.
List.replaceAll
Replaces each element with the result of applying a UnaryOperator:
List<String> names = new ArrayList<>(Arrays.asList(" alice ", "BOB", "Charlie"));
// Java 8
names.replaceAll(String::trim);
names.replaceAll(String::toLowerCase);
// names is now ["alice", "bob", "charlie"]
// Or in one pass:
names.replaceAll(s -> s.trim().toLowerCase());
Equivalent to:
for (int i = 0; i < names.size(); i++) {
names.set(i, names.get(i).trim().toLowerCase());
}
List.sort
List.sort(Comparator) is a default method that delegates to Arrays.sort:
List<String> names = new ArrayList<>(Arrays.asList("Charlie", "Alice", "Bob"));
names.sort(Comparator.naturalOrder()); // [Alice, Bob, Charlie]
names.sort(Comparator.reverseOrder()); // [Charlie, Bob, Alice]
names.sort(Comparator.comparingInt(String::length)); // [Bob, Alice, Charlie]
names.sort(null); // null = natural order for Comparable types
Map Enhancements
The Map interface gained the most new methods. They solve common patterns that previously required verbose imperative code.
getOrDefault
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
// Before Java 8
int score = scores.containsKey("Bob") ? scores.get("Bob") : 0;
// Java 8
int score = scores.getOrDefault("Bob", 0); // 0
putIfAbsent
// Only puts if key is absent (or mapped to null)
scores.putIfAbsent("Alice", 0); // does nothing — Alice already has a score
scores.putIfAbsent("Dave", 0); // adds Dave with score 0
computeIfAbsent
Like putIfAbsent but the value is computed lazily — only invoked when the key is absent:
Map<String, List<String>> groups = new HashMap<>();
// Before Java 8 — verbose
if (!groups.containsKey("admin")) {
groups.put("admin", new ArrayList<>());
}
groups.get("admin").add("Alice");
// Java 8 — concise
groups.computeIfAbsent("admin", k -> new ArrayList<>()).add("Alice");
This is the most useful Map method in Java 8. Use it to build multi-value maps, caches, and lazy-init patterns.
// Building an inverted index
Map<Character, List<String>> byFirstLetter = new HashMap<>();
for (String word : words) {
byFirstLetter.computeIfAbsent(word.charAt(0), k -> new ArrayList<>()).add(word);
}
// Memoisation cache
Map<Integer, Long> memo = new HashMap<>();
long fib(int n) {
return memo.computeIfAbsent(n, k -> k <= 1 ? k : fib(k-1) + fib(k-2));
}
computeIfPresent
Computes a new value only if the key already exists with a non-null value:
// Increment score only if player exists
scores.computeIfPresent("Alice", (key, value) -> value + 10);
// If Alice has 95, she now has 105
// If key absent: no-op
// Remove from map if computed value is null
scores.computeIfPresent("Alice", (key, value) -> value <= 0 ? null : value - 1);
// Removes Alice from map if score drops to 0
compute
Computes a value regardless of whether the key is present:
// Word frequency counter
Map<String, Integer> freq = new HashMap<>();
for (String word : words) {
freq.compute(word, (k, v) -> v == null ? 1 : v + 1);
}
// Equivalent, but merge is cleaner for this pattern (see below)
If the function returns null, the key is removed from the map.
merge
The most powerful accumulation method. Takes a key, a value, and a merge function that combines the existing value with the new one:
// Accumulate: freq.merge(word, 1, (existing, newVal) -> existing + newVal)
Map<String, Integer> freq = new HashMap<>();
for (String word : words) {
freq.merge(word, 1, Integer::sum);
}
// If key absent: puts 1. If present: adds 1 to existing count.
More examples:
// Concatenate strings per key
Map<String, String> log = new HashMap<>();
log.merge("error", "NPE\n", String::concat);
log.merge("error", "IOE\n", String::concat);
// log.get("error") == "NPE\nIOE\n"
// Remove when accumulated value reaches threshold
inventory.merge(item, -quantity, (existing, delta) -> {
int newQty = existing + delta;
return newQty <= 0 ? null : newQty; // null removes the key
});
Rule of thumb: Use merge for accumulation patterns (counting, summing, concatenating). Use computeIfAbsent for lazy initialisation. Use compute when you need full control.
replaceAll
Applies a BiFunction to each entry and replaces the value:
// Double all scores
scores.replaceAll((name, score) -> score * 2);
// Apply a bonus based on the key
scores.replaceAll((name, score) -> name.startsWith("V") ? score + 10 : score);
forEach
Iterates all entries with a BiConsumer:
scores.forEach((name, score) -> System.out.println(name + ": " + score));
// With filtering (combine with entrySet().stream() for more complex cases)
scores.forEach((name, score) -> {
if (score >= 90) System.out.println("Top performer: " + name);
});
remove(key, value)
Removes an entry only if it maps the key to the specified value (atomic check-and-remove):
boolean removed = scores.remove("Alice", 95); // removes only if Alice's score == 95
replace(key, value) and replace(key, oldValue, newValue)
scores.replace("Alice", 100); // unconditional replace (only if key exists)
scores.replace("Alice", 95, 100); // conditional: replace 95 → 100
Map.Entry Comparators
Java 8 added static comparators to Map.Entry for sorting entry streams:
Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 87, "Charlie", 92);
// Sort entries by value (score)
scores.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));
// Alice: 95, Charlie: 92, Bob: 87
// Sort by key
scores.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));
Practical: Word Frequency in One Pass
String text = "to be or not to be that is the question to be";
Map<String, Long> freq = Arrays.stream(text.split("\\s+"))
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
// Sort by frequency descending
freq.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(5)
.forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));
// to: 3, be: 3, or: 1, not: 1, that: 1
Summary
| Method | Type | Use case |
|---|---|---|
forEach(Consumer) | Iterable | Iterate without loop |
removeIf(Predicate) | Collection | Conditionally remove |
replaceAll(UnaryOperator) | List | Transform in place |
sort(Comparator) | List | Sort in place |
getOrDefault(k, def) | Map | Safe get with fallback |
putIfAbsent(k, v) | Map | Put only if absent |
computeIfAbsent(k, fn) | Map | Lazy init on first access |
computeIfPresent(k, fn) | Map | Update only if present |
compute(k, fn) | Map | Full control compute |
merge(k, v, fn) | Map | Accumulate / combine |
replaceAll(BiFunction) | Map | Transform all values |
forEach(BiConsumer) | Map | Iterate key-value pairs |
Next Step
CompletableFuture: Async Pipelines and Non-Blocking Composition →
Part of the DevOps Monk Java tutorial series: Java 8 → Java 11 → Java 17 → Java 21