The first step, “Creation,” is where you generate a stream. When generated, a stream has not yet done
anything with the underlying data, just as an I/O stream has not read any bytes when you create it. The
stream simply understands how to start generating data, and it provides a rich API and a significant amount
of metadata for the generation of that data.
You can generate a stream from a Collection using the stream() or parallelStream() method. The
stream() method generates a sequential (non-parallel) stream, which is useful when you need elements of
the Collection to be processed in iteration order. If you do not have any order-of-execution requirements,
however, it is always better to ask for a stream using parallelStream() . That method gives permission to the
generated stream to process the elements in any order, including processing them concurrently.
It is worth noting that you may still get a sequential stream from parallelStream() if the Collection
can't support parallelism: calling parallelStream is not demanding a stream that is parallelizable, but is
giving permission for the returned stream to be parallelizable. These methods provide a bridge from the
Collection API to the Stream API, so it is easy to migrate your code. 1 Similarly, if you want to generate a
stream from an array, then use the Stream.of(array) method.
At the lowest level, streams can be generated using the static methods on the Stream interface. In
particular, you can generate a Stream based on the Supplier functional interface using the generate
method, and you can create a stream that iterates based on some initial value and an incrementing lambda
using the iterate method (e.g., pass in 0 and x->x+1 as the lambda to create a stream that counts up
from zero). Finally, the Stream class provides a builder class via the builder() method, so you can pass in
elements to the builder and then generate the stream based on the results.
When we get into stream generation, we are entering into the lair of these strange beasts called
“spliterators.” Spliterators can be thought of as the larval form of streams: they enter into the cocoon through
the StreamSupport.stream method and emerge as a fully formed Stream instance. They are complex
enough in their own right to warrant a lengthier discussion, though, so we will get into spliterators at length
when we talk about lambda and stream implementation details in Chapter 9. For the most part, users
can and should stick to the Stream interface itself: rely on library support to obscure away the spliterator
The streams you see here and in the Java SDK are not the only possibilities. Webonise Lab's FunJava
library provides other ways to generate streams: In specific, that library provides a stream based on a
database ResultSet , based on file contents, based on directory structures, and based on InputStream and
Reader instances. We will get into the details about those stream generators in the chapters on processing
files and processing database calls using streams, and you can use those implementations as models for your
own stream implementations. For the sake of focus, however, in this chapter we will stick to those simpler
streams provided by Java itself.
Mapping and Filtering Streams
Once you have a stream created, what can you do with it? Operations on streams are broken down into two
different kinds: intermediate and terminal operations. The intermediate steps are steps that can be chained
together, whereas the terminal steps terminate the stream. The processing of the stream is not kicked off
until the stream reaches a terminal step, so the intermediate steps can be chained together and manipulated
before the stream initiates any kind of heavyweight activity. There are two basic kinds of intermediate steps:
map steps and filter steps.
1 For information on how to bridge back to the Collection API from the Stream API , see the collecting/reducing step
description below and the Collectors class.