What is the Stream API, and how do intermediate and terminal operations differ?
Quick Answer
A Stream represents a sequence of elements supporting a pipeline of functional-style operations, without storing the elements itself and without modifying the underlying source. Intermediate operations (filter, map, sorted) are lazy and return a new Stream, only actually running once a terminal operation triggers the pipeline. Terminal operations (collect, forEach, reduce, count) consume the stream, produce a final result or side effect, and cannot be followed by any further operations on that stream.
Detailed Answer
A Stream<T> represents a (possibly lazy, possibly infinite) sequence of elements that supports a chain of functional-style operations, typically built from a collection (list.stream()), an array, or a generator. Crucially, a stream doesn't store data itself and doesn't modify its source — it's a pipeline description, evaluated on demand.
Intermediate operations (filter, map, sorted, distinct, limit, ...) return a new Stream, and are lazy — nothing actually executes until a terminal operation is invoked. This laziness lets the pipeline be optimized as a whole (e.g., fusing filter+map into a single pass per element, or short-circuiting with limit).
Terminal operations (collect, forEach, reduce, count, anyMatch, toList) trigger the actual traversal, produce a final result (or side effect), and consume the stream — you cannot reuse or chain further operations onto a stream after a terminal operation runs (attempting to reuses throws IllegalStateException).
List<String> names = List.of("Alice", "Bob", "Charlie", "Dave");
List<String> result = names.stream() // create
.filter(n -> n.length() > 3) // intermediate — lazy, not yet run
.map(String::toUpperCase) // intermediate — lazy, not yet run
.sorted() // intermediate — lazy, not yet run
.collect(Collectors.toList()); // terminal — pipeline actually executes now
Because intermediate operations are lazy, names.stream().filter(...) alone does nothing observable — no exception is even thrown for a filter predicate that would fail on some element, until a terminal operation actually pulls elements through the pipeline. This lazy, pull-based evaluation (each element flows through the entire pipeline one at a time, rather than each stage processing the whole collection before the next) is what makes operations like limit() able to short-circuit an otherwise-infinite stream.