Part 12 of 16

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

MethodTypeUse case
forEach(Consumer)IterableIterate without loop
removeIf(Predicate)CollectionConditionally remove
replaceAll(UnaryOperator)ListTransform in place
sort(Comparator)ListSort in place
getOrDefault(k, def)MapSafe get with fallback
putIfAbsent(k, v)MapPut only if absent
computeIfAbsent(k, fn)MapLazy init on first access
computeIfPresent(k, fn)MapUpdate only if present
compute(k, fn)MapFull control compute
merge(k, v, fn)MapAccumulate / combine
replaceAll(BiFunction)MapTransform all values
forEach(BiConsumer)MapIterate key-value pairs

Next Step

CompletableFuture: Async Pipelines and Non-Blocking Composition →

Part of the DevOps Monk Java tutorial series: Java 8Java 11Java 17Java 21