Definition
- Signature –
collectingAndThen(Collector.downstream, Function finisher) - Behavior – the downstream collector collects normally; then the finisher transforms the result.
- Wrapping –
collectingAndThen(toList(), Collections::unmodifiableList)makes the list read-only. - Unwrapping –
collectingAndThen(maxBy(comp), opt -> opt.orElseThrow())extracts the value from Optional. - Converting –
collectingAndThen(toList(), List::size)returns the size instead of the list.
collectingAndThen is like a post office: you collect all the letters (downstream collector), then stamp and seal the envelope (finisher). One operation handles both the collection and the final packaging.
Understanding the Concepts
What is collectingAndThen?
collectingAndThen runs a downstream collector and then applies a finisher transformation to the result.
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
// Wrap collected list in unmodifiableList
List<String> readOnly = trades.stream()
.map(Trade::getTradeId)
.collect(Collectors.collectingAndThen(
Collectors.toList(),
Collections::unmodifiableList));
// readOnly.add("X") -> UnsupportedOperationException
How do you extract a value from Optional?
import java.util.Comparator;
// maxBy returns Optional; finisher unwraps it
Trade largest = trades.stream()
.collect(Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparing(Trade::getAmount)),
opt -> opt.orElseThrow(() -> new RuntimeException("No trades"))));
How do you convert the collected type?
// Collect to list, then get size
int count = trades.stream()
.filter(Trade::isSettled)
.collect(Collectors.collectingAndThen(
Collectors.toList(),
List::size));
// Collect to list, then convert to array
String[] ids = trades.stream()
.map(Trade::getTradeId)
.collect(Collectors.collectingAndThen(
Collectors.toList(),
list -> list.toArray(String[]::new)));
How does it work inside groupingBy?
import java.util.Map;
// Per-group unmodifiable lists
Map<String, List<Trade>> byBranch = trades.stream()
.collect(Collectors.groupingBy(Trade::getBranch,
Collectors.collectingAndThen(
Collectors.toList(),
Collections::unmodifiableList)));
// Per-group: get the single largest trade
Map<String, Trade> largestPerBranch = trades.stream()
.collect(Collectors.groupingBy(Trade::getBranch,
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparing(Trade::getAmount)),
opt -> opt.orElseThrow())));
What about Java 10+ toUnmodifiableList?
Java 10 added toUnmodifiableList() which replaces the most common collectingAndThen use case.
// Java 8-9: collectingAndThen pattern
List<String> readOnly = stream.collect(
Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
// Java 10+: direct method
List<String> readOnly = stream.collect(Collectors.toUnmodifiableList());
// Java 16+: even simpler
List<String> readOnly = stream.toList(); // already unmodifiable
How do you chain multiple finishers?
// Cannot chain directly, but compose the finisher function
import java.util.function.Function;
Function<List<Trade>, List<Trade>> sortThenUnmod =
((Function<List<Trade>, List<Trade>>) list -> {
list.sort(Comparator.comparing(Trade::getAmount));
return list;
}).andThen(Collections::unmodifiableList);
List<Trade> result = trades.stream()
.collect(Collectors.collectingAndThen(Collectors.toList(), sortThenUnmod));
What are common mistakes?
// WRONG: collecting then post-processing in two steps
List<Trade> list = trades.stream().collect(Collectors.toList());
List<Trade> readOnly = Collections.unmodifiableList(list);
// CORRECT: single collect call with collectingAndThen
List<Trade> readOnly = trades.stream().collect(
Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
What should a senior engineer know?
- Java 10+
toUnmodifiableList()replaces the commoncollectingAndThen(toList(), unmodifiableList)pattern. - collectingAndThen is the general post-processing tool. Use it for any transformation after collection.
- The finisher runs synchronously after the downstream collector completes.
- For parallel streams, the downstream collector handles parallelism; the finisher runs once on the final result.
- Compose multiple finishers with
f1.andThen(f2)as the finisher function.
Visual Overview
collectingAndThen(toList(), Collections::unmodifiableList):
Stream: "TRD-001" → "TRD-002" → "TRD-003"
│ │ │
▼ ▼ ▼
Downstream (toList):
["TRD-001", "TRD-002", "TRD-003"]
│
▼
Finisher (unmodifiableList):
UnmodifiableList["TRD-001", "TRD-002", "TRD-003"]
│
▼
.add("X") → UnsupportedOperationException
Key Takeaway
collectingAndThen applies a finisher transformation after the downstream collector completes. The three primary uses are wrapping (make the result unmodifiable), unwrapping (extract a value from Optional returned by maxBy/minBy), and converting (collect to list, finisher converts to array or counts size).
It is especially useful as a downstream inside groupingBy to produce clean per-group results without post-processing. Java 10+ toUnmodifiableList() replaces the most common use case, but collectingAndThen remains the general tool for any arbitrary finisher.
Remember one sentence: collectingAndThen runs a collector then applies a finisher, useful for wrapping, unwrapping, or converting the collected result in one step.
---