Reducing & Collecting
Reducing & Collecting
The previous lessons showed you how to filter and transform stream elements. Those operations produce a new stream — but at some point you need a concrete answer: a single number, a list, a map. That is what terminal operations are for. This lesson covers the three most essential ones: reduce, count, and collecting results with Collectors.toList() (and its modern replacement).
Why terminal operations matter
A stream pipeline is lazy. The intermediate steps (filter, map, etc.) are not evaluated until a terminal operation is called. reduce and collect are the two most powerful terminal operations: one folds a stream into a single value, the other pours elements into a mutable container such as a List or Map.
count — the simplest terminal operation
count() returns a long — the number of elements that survive the pipeline.
count() is conceptually equivalent to reduce(0L, (acc, e) -> acc + 1) — which brings us to the more general operation.
reduce — folding a stream into one value
reduce applies a binary operator repeatedly until the stream is exhausted. Think of it like a fold over a list: you start with an identity value and combine it with each element one by one.
The most common overload takes an identity and a BinaryOperator<T>:
The identity value must be a true identity for the operation — adding 0 never changes the result, so 0 is the identity for addition. For multiplication the identity is 1.
You can also write the lambda explicitly to make the mechanics clear:
reduce without an identity — Optional
When no identity is provided, reduce returns an Optional<T> because the stream might be empty and there would be no meaningful result to return.
Optional.
Collecting results with Collectors.toList()
reduce is great for computing a single scalar. But often you want a new collection. That is what collect does — it accumulates stream elements into a mutable container.
The most common collector is one that produces a List:
Since Java 16 there is a more concise alternative: Stream.toList(). It returns an unmodifiable list, which is usually what you want:
.toList() over Collectors.toList() in Java 16+ code. The short form is cleaner and signals immutability upfront. Use Collectors.toList() (or Collectors.toUnmodifiableList()) when you need to target Java 11/8 compatibility.
Collecting into other containers
The Collectors class offers many more collectors — you will explore them deeply in Lesson 7. For now, the two most useful after toList() are:
Collectors.toSet()— produces aSet, removing duplicates automatically.Collectors.joining(delimiter)— concatenatesStringstream elements into oneString.
Combining reduce and map — a practical example
A typical real-world pattern maps objects to a numeric property and then reduces to a summary statistic:
reduce for side effects. The lambda you pass to reduce must be stateless, non-interfering, and associative (so that parallel streams give the same result). Mutating external state inside the lambda is a common bug that silently breaks parallel pipelines.
Summary
count() is the simplest way to count elements after filtering. reduce lets you fold any stream into a single value — use the identity form when an empty stream should return a neutral result, and the Optional form when an empty stream is genuinely possible. collect(Collectors.toList()) — or the modern .toList() — pours a stream back into a concrete collection. These three operations cover the vast majority of stream terminal needs; the next lessons build on them.