🌊 Streams API · Intermediate

Streams are single-use in Java

Reusing a consumed stream throws IllegalStateException.

🧩 The mysteryYou save a stream in a variable to reuse it — count it, then find its max. The second call explodes. Can't a variable be used twice?

A one-ride ticket

A Stream object can be traversed only once. One stream = one pipeline = one terminal op. After that it's consumed, and using it again throws **IllegalStateException** ("stream has already been operated upon or closed").

🔮 Predict it

Ride it twice

What happens?

Stream<Integer> s = Stream.of(1, 2, 3);
System.out.println(s.anyMatch(n -> n > 2));
System.out.println(s.count());
  1. true 3
  2. Prints true, then throws IllegalStateException
  3. Compile error
Show the answer

It prints true, then the second terminal op finds the stream already consumed and throws IllegalStateException. It compiles fine — the check happens at runtime.

🤔 Think first

Sneaky consumption

Stream<String> h = words.filter(w -> w.startsWith("h")); — can you still call words.count()?

Think about it, then reveal the answer

No. Chaining an op onto words links it into the new pipeline h. Touching words again — even just to count — throws IllegalStateException. Only h may be used now.

Needing two answers

✗ Reuses a consumed stream
Stream<Integer> s = list.stream();
long n = s.count();
int max = s.max(Integer::compare).get();

Throws on the second line that uses s. There's no reset() method.

✓ Fresh stream each time
long n = list.stream().count();
int max = list.stream()
    .max(Integer::compare).get();

Streams are cheap to create: just ask the source for a new one.

Designing APIs

If callers need to iterate a result several times, don't return a Stream — the second caller would crash. Return a collection (List<Order>), or a **Supplier<Stream<T>>** that makes a fresh stream on demand.

Supplier<Stream<String>> names =
    () -> people.stream().map(Person::name);
names.get().count();     // fresh stream
names.get().findFirst(); // another one
💼 In the real world

Where it bites

The classic production version: a method returns a Stream, one caller logs its size, the next caller processes it — boom, IllegalStateException. Many teams have a simple rule: methods return collections, and streams stay local.

Key takeaways

  1. One stream object = one pipeline = one terminal op
  2. Reuse throws IllegalStateException
  3. Need it twice? Call list.stream() again
  4. Return collections (or a Supplier<Stream>) from APIs
🤯 Did you know?

Iterator has the same one-shot nature, but it doesn't throw on reuse: an exhausted iterator just keeps answering hasNext() with false.

Practice questions

What does this print?

Stream<String> s = Stream.of("a", "b");
System.out.println(s.count());
System.out.println(s.count());
  1. 2 2
  2. 2 0
  3. Prints 2, then throws IllegalStateException
  4. Compile error
Check your answer

Prints 2, then throws IllegalStateException. The first count() consumes the stream. The second call finds it already operated upon and throws IllegalStateException.

You need both the count and the max of the same list. What's the best fix?

Stream<Integer> s = list.stream();
long n = s.count();
int max = s.max(Integer::compare).get();
  1. Call list.stream() again for the second operation
  2. Call s.reset() before calling max
  3. Wrap the stream in parallel() so it can be reused
  4. Store the stream in a static field
Check your answer

Call list.stream() again for the second operation. Streams are cheap to create, so just make a new one from the source: list.stream().count() and list.stream().max(...). There is no reset method.

Next: infinite streams — rivers that never end — and the operators that know when to stop.