Modern Java Collection Enhancements: getOrDefault, computeIfAbsent, removeIf, and Comparator Utilities

Part 7 of 9 in Functional Java Unleashed


The quiet improvements

Working with collections in Java has always meant managing nulls, boilerplate branches, and imperative loops. Java 8 introduced several methods to the Map interface (and, via Collection, to all lists and sets) that eliminate entire categories of repetitive code.

This post walks through four modern APIs — getOrDefault, computeIfAbsent, removeIf, and the Comparator utility chain — each replacing a well-known boilerplate pattern with a single method call.

getOrDefault: no more null checks

The most common Map idiom in Java has always been the get-null-check-default dance:

Integer count = map.get("key");
if (count == null) {
    count = 0;
}

getOrDefault collapses this into one call. If the key is present it returns the value; if absent it returns the default — no branches needed.

But Map.getOrDefault doesn’t compute a value when the key exists, which is exactly right for simple defaults like 0. When the default itself is expensive to create (building a new list, querying a database), that’s where computeIfAbsent comes in.

The first two lines confirm both approaches produce the same result for an existing key. The third line shows the key being absent — getOrDefault returns the default 0. The last section demonstrates computeIfAbsent: on first access it runs the lambda (you can see the diagnostic print), stores the computed value, and on second access skips the lambda entirely because the key now exists.

The practical takeaway: use getOrDefault(k, defaultValue) when the default is a cheap constant or already-computed value. Use computeIfAbsent(k, factory) when creating the default is expensive — the factory only runs once, the first time the key is requested.

computeIfAbsent: lazy initialization made trivial

Without computeIfAbsent, building a map of lists (e.g., grouping items by category) requires a null check and an explicit put every time:

List<String> emails = map.get("engineering");
if (emails == null) {
    emails = new ArrayList<>();
    map.put("engineering", emails);
}
emails.add("[email protected]");

With computeIfAbsent the entire block becomes:

map.computeIfAbsent("engineering", k -> new ArrayList<>())
   .add("[email protected]");

The output confirms three things. The manual-init block (five lines) and the computeIfAbsent call (one line) produce identical results. Crucially, the second computeIfAbsent("marketing", ...) call does not execute the lambda — you would see its print statement if it did, but only one marketing list exists.

The grouping example at the end shows the most common real-world pattern: iterating a collection and distributing items into a Map<Integer, List<String>> keyed by string length. The lambda runs once per unique key value, then returns the same list for all subsequent calls with that key.

removeIf: deleting by condition without a temporary collection

Before Java 8’s removeIf, removing elements matching a predicate meant iterating a copy or using an Iterator.remove() in a loop. With it, you write the condition and let the collection handle iteration.

On List and Set it’s a direct method call. On Map you call entrySet().removeIf(...) — there is no Map.removeIf because maps have two values per entry.

The inventory cleanup pattern at the top demonstrates this in action: a single pass removes zero-stock items, then a second pass removes negative values (invalid data). The List example shows removing scores below zero, and the Set example strips words shorter than four characters.

On Map, note that the predicate receives (key, value) — not just one or the other. This matters when your deletion criteria involves the key (e.g., “remove all entries whose key starts with a specific prefix”).

Comparator utilities: building sort criteria without anonymous classes

Sorting by multiple fields in Java used to mean nested if comparisons inside an anonymous Comparator. The utility chain — comparing(), thenComparing(), reversed(), and nullsFirst()/nullsLast() — makes complex ordering readable.

Each sorted list in the output is a copy of the same data, sorted differently. The first sorts by name (lexicographic), showing that Comparator.comparing extracts the field and delegates to its natural order.

The second sorts by age ascending with nullsLast, then breaks ties by name — Bob comes before Dave at age 25 because “B” < “D”. Alice and Charlie both have age 30, so they sort alphabetically too.

reversed() flips the entire chain. With nullsLast in the base comparator, reversing still places nulls last but now in descending order (the null stays at the end while ages flip from ascending to descending).

nullsFirst and nullsLast handle the edge case of nullable fields gracefully — without them, any comparison involving a null field throws NullPointerException.

The final example shows nullsFirst combined with reversed: ages go 35 → 25 (descending), then Frank’s null lands at the bottom because nullsFirst was applied before the reversal (nulls are sorted first as if “less than everything,” and reversing puts them at the end of descending order).

Takeaway

These four APIs share a common pattern: they replace multi-line imperative boilerplate with single, readable method calls. getOrDefault handles simple lookups with defaults; computeIfAbsent adds lazy initialization on top. removeIf eliminates temporary collections in delete-by-condition loops. The Comparator chain replaces nested comparisons with composable steps — and handles nulls explicitly rather than throwing. Together they cover the most common collection manipulation patterns without requiring any library imports beyond java.util.