Introduction to Streams
Introduction to Streams
The Streams API, introduced in Java 8 and refined ever since, is one of the most significant additions to the language. It lets you express data-processing logic as a declarative pipeline — telling Java what to compute rather than how to iterate. Before going further, it is important to be precise about what a stream actually is, because the word is overloaded in computing.
What Is a Stream?
A Stream<T> in Java is a sequence of elements that supports sequential and parallel aggregate operations. Three things make it distinct from a plain array or list:
- No storage. A stream does not hold data. It reads from a source (a collection, an array, a file, a generator) and passes elements through the pipeline on demand.
- Functional in nature. Operations produce new streams or a final result; they never mutate the source.
- Lazily evaluated. Intermediate operations are not executed until a terminal operation demands a result.
java.util.stream.Stream is completely unrelated to java.io.InputStream or java.io.OutputStream. Both are called "streams," but they solve different problems. This tutorial covers only java.util.stream.
Streams vs. Collections — The Key Difference
A collection (like ArrayList or HashSet) is a data structure: it stores elements in memory, lets you add and remove them, and lets you iterate over them in any order you like. A stream is a view for processing that data; it is consumed once and then discarded.
Consider a concrete example. Suppose you have a list of product names and you want the names that start with "A", converted to uppercase.
Collection-style (imperative):
Stream-style (declarative):
Both produce the same answer. The stream version reads like a specification: filter names that start with A, then map each to uppercase, then collect into a list. There is no explicit loop, no mutation of an accumulator, and no index to manage.
The Pipeline Model
Every stream expression follows the same three-stage structure:
- Source — where elements come from.
- Intermediate operations — zero or more transformations that return a new stream.
- Terminal operation — triggers execution and produces a result (or a side-effect).
Nothing runs until the terminal operation is reached. If you call filter(...).map(...) without a terminal operation, Java does nothing — the lambda bodies are never invoked. This laziness is intentional: it lets the runtime fuse operations and avoid creating intermediate collections.
Laziness in Practice
To see laziness clearly, add a print statement inside an intermediate operation:
Output:
The word "About to trigger..." prints before any filtering, proving that the intermediate operation was deferred until count() was called.
A Stream Is Single-Use
Once a terminal operation has been called, the stream is consumed. Attempting to reuse it throws IllegalStateException.
.stream() on the source collection again — that is cheap.
Quick Summary
- A
Stream<T>is a lazy, single-use pipeline over a data source — it stores nothing. - Collections own data; streams process it.
- Every stream has a source, zero or more intermediate operations, and exactly one terminal operation.
- Nothing executes until the terminal operation is invoked.
In the next lesson you will see the many ways Java lets you create a stream — from collections, arrays, ranges, files, and generators.