JavaStream API

Stream API

Introduced in Java 8, the Stream API provides a functional-style way to process sequences of elements — filtering, transforming, and combining data — without writing explicit loops and mutable accumulator variables. Streams pair naturally with the lambdas and method references covered on the previous pages.

A stream is not a data structure
Note
This is the single most important thing to understand about streams: a `Stream` does not store any elements. It is a **pipeline of computations** over a *source* of data — a collection, an array, or a generator function. The source holds the data; the stream just describes what to do with it.

Because a stream holds no data of its own, it cannot be indexed, cannot be iterated more than once, and does not support adding or removing elements the way a List or Set does. Its entire job is to describe a sequence of operations, which only run when you ask for a result.

The three-part pipeline

Every stream pipeline has the same three-part shape:

  • Source — where the elements come from, most often .stream() on a Collection.

  • Intermediate operations — steps like .filter() or .map() that transform the stream; there can be zero, one, or many of these, chained together.

  • Terminal operation — a single final step like .collect() or .forEach() that actually produces a result and consumes the stream.

Creating a stream from a collection

Java
List<String> names = List.of("Alice", "Bob", "Charlie", "Dave");

List<String> longNames = names.stream()               // source
    .filter(name -> name.length() > 3)                 // intermediate operation
    .map(String::toUpperCase)                           // intermediate operation
    .collect(Collectors.toList());                      // terminal operation

System.out.println(longNames);  // [ALICE, CHARLIE, DAVE]
Streams are lazy
Note
Intermediate operations like `.filter()` and `.map()` are **lazy** — calling them does not process a single element. They just build up the description of the pipeline. Nothing actually runs until a terminal operation is invoked, at which point the whole pipeline executes in one pass over the source.

Nothing happens until a terminal operation runs

Java
Stream<String> pipeline = names.stream()
    .filter(name -> {
        System.out.println("filtering " + name);
        return name.length() > 3;
    });

System.out.println("Pipeline built, nothing printed yet.");

long count = pipeline.count();   // NOW the filter actually runs, once per element
Streams are single-use
Warning
A stream can only be consumed by **one** terminal operation. Once a terminal operation has run, that stream is closed — calling any operation on it again throws `IllegalStateException`. If you need to process the same data twice, call `.stream()` on the source again to get a fresh stream.

Reusing a consumed stream throws IllegalStateException

Java
Stream<String> stream = names.stream();
stream.forEach(System.out::println);   // consumes the stream

stream.forEach(System.out::println);   // throws IllegalStateException: stream has already been operated upon or closed
  • A Stream is a pipeline of operations over a source, not a data structure — it stores nothing itself.

  • Every pipeline follows source → intermediate operations → terminal operation.

  • Intermediate operations are lazy — they build the pipeline but do not execute until a terminal operation runs.

  • A stream can be consumed by exactly one terminal operation; reusing it after that throws IllegalStateException.