Skip to main content

Week 7 Day 2 — Stream Pipelines and Laziness

Goal

Today I want a stream as a lazy pipeline that runs once, not as a new kind of list.

Main questions:

  1. What is a stream?
  2. What is intermediate vs terminal?
  3. Why is the pipeline lazy?
  4. What does “consumed once” mean?
  5. Which operations short-circuit or buffer?

1. A stream is not a collection

A Stream<T> is a view of a pipeline: source → intermediate ops → terminal op.

List<String> names = orders.stream() // source
.filter(Order::isOpen) // intermediate
.map(Order::customerName) // intermediate
.toList(); // terminal (Java 16+)

The list orders still exists. The stream does not store the filtered names until the terminal operation runs. After toList(), the stream is spent.

Memory sentence:

Streams do nothing until a terminal operation; then they are spent.


2. Intermediate vs terminal

Intermediate (lazy, return Stream)Terminal (eager, consume)
filter, map, flatMapcollect, toList, reduce
distinct, sorted, peekforEach, count, min, max
limit, skip, takeWhilefindFirst, findAny
anyMatch, allMatch, noneMatch

Intermediate ops build the pipeline. They do not walk the source. Terminal ops run it and close it.

Stream<Order> s = orders.stream().filter(Order::isOpen);
// nothing has been filtered yet
List<Order> open = s.toList(); // now it runs
s.toList(); // IllegalStateException: stream has already been operated upon or closed

3. Laziness and short-circuit

Laziness lets later stages stop early.

orders.stream()
.filter(Order::isOpen)
.map(this::expensiveDto)
.findFirst();

findFirst can complete after the first open order. expensiveDto does not run for the rest. That is short-circuit.

anyMatch, allMatch, noneMatch, findFirst, findAny, limit short-circuit.

count() may skip map if the size is known and the map has no side effects. Do not put business side effects in map and expect them to run for count().


4. Stateful intermediate ops

Most intermediate ops are stateless: each element is independent (map, filter).

Stateful ops look at more than one element:

  • sorted — buffers all remaining elements, then sorts
  • distinct — remembers what it has seen
  • limit / skip — need a count (still lazy, but ordered streams pay more in parallel)

A pipeline filter.sorted.findFirst cannot short-circuit before sort: sort must see everything that passed the filter.

orders.stream()
.filter(Order::isOpen)
.sorted(Comparator.comparing(Order::createdAt))
.findFirst(); // sort still buffers all open orders

peek is an intermediate Consumer for debugging. It is not a substitute for forEach. Relying on peek for persistence or logging is a side-effect bug when the pipeline short-circuits or when count elides it.


5. Spring connection

  • repository.findAll().stream() on a large table loads all rows into memory first if findAll returns a List. Laziness of the stream does not make JPA lazy. Use query filters and pagination.
  • Streaming a JPA lazy collection outside a transaction: LazyInitializationException. The pipeline runs when the session is gone.
  • Files.lines(path) is a stream that must be closed. Use try-with-resources (Week 4).
  • Do not pass a Stream out of a @Transactional method and let the controller collect it later — the persistence context may already be closed.

6. Common traps

Trap 1: Treating Stream like a reusable List.

Trap 2: Forgetting a terminal operation and wondering why nothing happens.

Trap 3: sorted then assuming findFirst avoided the rest of the work.

Trap 4: peek for auditing in production.

Trap 5: stream() on findAll() as a performance strategy.


Practice Questions and Answers

Question 1

Intermediate vs terminal operations? Why lazy?

Answer:

Intermediate ops return a new stream and record work without running it. Terminal ops consume the stream and trigger the source walk. Laziness allows fusion and short-circuit: filter.map.findFirst can stop after one hit. After the terminal op, the stream cannot be reused.


Question 2

What happens if I call collect twice on the same stream?

Answer:

The second call throws IllegalStateException. I rebuild from the source: orders.stream()... again.


Question 3

Does filter.map.findFirst run map for every element?

Answer:

No. After the first element that passes filter, findFirst completes. Remaining map calls do not run. If I insert sorted before findFirst, all filtered elements are buffered.


Question 4

Is a stream a collection?

Answer:

No. A collection stores elements. A stream describes how to produce a result from a source. Size may be unknown or infinite (Stream.iterate) until I limit and terminate.


Question 5

Why is peek a poor place for business logic?

Answer:

peek may not run for every element (short-circuit, optimization). It exists to debug the pipeline. Auditing, saving, and sending mail belong in a terminal op I control, or in a plain loop.


Memory sentences

Streams do nothing until a terminal operation; then they are spent.

Intermediate builds the pipeline. Terminal runs it.

sorted buffers. findFirst can stop early only if nothing before it needed the whole stream.

Next: Week 7 Day 3 — map, flatMap, and filter