The Streams API
java2-3's functional interfaces weren't just for standalone use — they're the building blocks Streams are made of. This chapter puts Function, Predicate, and method references to work in a real pipeline.
What a Stream Is
A Stream is not a collection — it stores nothing. It's a one-shot pipeline over a data source (a List, an array, a file), describing a sequence of operations to run over that source's elements.
Building a Pipeline — map/filter/reduce
.stream() opens a pipeline from a source; filter and map are intermediate operations that describe transformations without executing them; collect is a terminal operation that finally runs the whole pipeline and produces a result.
Lazy Evaluation — The Genuine Surprise
Intermediate operations don't run when they're written — they only run once a terminal operation triggers the whole pipeline. Building the pipeline above prints nothing at all, because filter alone is never enough to force evaluation.
Comparison to C++'s STL Algorithms and Rust's Iterator Adapters
cpp2-3's STL algorithms — std::sort, std::find — are standalone free functions operating on an iterator range; they don't chain into a pipeline the way Streams do. Rust's iterator adapters are the genuinely closest parallel on this site: .map().filter().collect() chains exactly the way a Java Stream does, and Rust's own iterators are lazy in the identical sense — an adapter chain does nothing until something actually consumes it, like collect() or a for loop.
Streams Can Only Be Consumed Once
A stream is a single-use pipeline. Once a terminal operation runs, that stream instance is spent — calling a second terminal operation on the same stream throws IllegalStateException at runtime, not a compile error.
| Aspect | C++ STL algorithms (cpp2-3) | Rust iterator adapters | Java Streams |
|---|---|---|---|
| Shape | standalone free functions | chainable adapter methods | chainable pipeline methods |
| Laziness | runs immediately when called | lazy until consumed | lazy until a terminal op runs |
| Reusable after use | n/a — operates on a range directly | iterator itself is consumed | no — single-use, throws if reused |
.map(String::toUpperCase) reads more directly than .map(s -> s.toUpperCase()) for the same result — reach for the method-reference form from java2-3 whenever a stream operation is nothing but a single existing-method call.
.stream() again from the source each time, since the stream object itself is genuinely single-use.
Coding Challenges
Given a List<Integer> of numbers, use a stream pipeline to filter out odd numbers, square the remaining ones, and collect the result into a new List, using method references where possible.
📄 View solutionBuild a stream pipeline with a filter lambda that prints a message as a side effect, but don't call any terminal operation. Run the program and explain in a comment why nothing prints.
📄 View solutionCreate a Stream, call a terminal operation on it (e.g. forEach), then call a second terminal operation on the same stream object. Show the resulting IllegalStateException and explain why it happens.
📄 View solutionChapter 4 Quick Reference
- A Stream is a one-shot pipeline over a data source, not a collection itself
- Intermediate operations (map, filter) are lazy — nothing runs until a terminal operation (collect, forEach, reduce) triggers the pipeline
- Closest parallel on this site: Rust's own lazy, chainable iterator adapters — C++'s STL algorithms (cpp2-3) run immediately and don't chain
- A stream is single-use — a second terminal operation on the same stream throws IllegalStateException
- Method references (java2-3) read cleanly inside stream pipelines whenever an operation is just an existing method call
- Next chapter: concurrency in Java — Thread, Runnable, and java.util.concurrent