Definition
- Single-use: a stream can be traversed only once.
- After a terminal (collect, forEach, count), the stream is consumed.
- Reuse throws:
IllegalStateExceptionwith 'stream has already been operated upon or closed'. - Fix: create a new stream from the source for each traversal.
- Store the source, not the stream, for multiple operations.
Once the conveyor belt at National Bank of India has run and emptied, Priya cannot run it again. She must set up a fresh belt from the same source.
Understanding the Concepts
What happens when you reuse a stream?
package com.bank.core;
import java.util.List;
import java.util.stream.Stream;
public class StreamReuseDemo {
public static void main(String[] args) {
List<String> accounts = List.of("SAV2001", "SAV2002", "SAV2003");
Stream<String> stream = accounts.stream();
// First terminal: works fine
long count = stream.count();
System.out.println("Count: " + count);
// Second operation on same stream: throws!
try {
stream.forEach(System.out::println);
} catch (IllegalStateException e) {
System.out.println("Error: " + e.getMessage());
}
}
}
Count: 3
Error: stream has already been operated upon or closed
Why are streams single-use?
Streams are pull-based: a terminal operation pulls elements through the pipeline, consuming them. After traversal, the stream's internal state is exhausted. The single-use contract enables lazy evaluation and pipeline fusion without requiring the stream to buffer all elements.
How do you process data multiple times?
List<String> accounts = List.of("SAV2001", "SAV2002", "SAV2003");
// Create a new stream for each operation
long count = accounts.stream().count();
List<String> sorted = accounts.stream().sorted().toList();
String first = accounts.stream().findFirst().orElse("none");
// Each call to accounts.stream() creates a fresh stream
What is the difference between the source and the stream?
| Aspect | Source (Collection) | Stream |
|---|---|---|
| Reusable | Yes: call stream() many times | No: single traversal |
| Stores data | Yes: holds elements in memory | No: lazy pipeline over source |
| Modifiable | Depends on type | Never: stream ops do not modify source |
What are common mistakes?
Mistake: Storing a stream in a variable and operating on it twice.
// WRONG: reusing the stream
Stream<String> s = accounts.stream().filter(id -> id.startsWith("SAV"));
long count = s.count(); // works
List<String> list = s.toList(); // throws IllegalStateException
// CORRECT: create fresh streams
long count = accounts.stream().filter(id -> id.startsWith("SAV")).count();
List<String> list = accounts.stream().filter(id -> id.startsWith("SAV")).toList();
What if you need to run multiple pipelines efficiently?
If re-streaming the source is expensive, materialize the intermediate result into a collection and stream that.
List<String> filtered = accounts.stream()
.filter(id -> id.startsWith("SAV"))
.toList();
long count = filtered.size();
String first = filtered.getFirst();
List<String> sorted = filtered.stream().sorted().toList();
What about using a Supplier?
import java.util.function.Supplier;
Supplier<Stream<String>> streamSupplier = () -> accounts.stream()
.filter(id -> id.startsWith("SAV"));
long count = streamSupplier.get().count();
List<String> list = streamSupplier.get().toList();
// Each get() creates a fresh stream
What should a senior engineer know?
Never store a stream in a field or pass it around for multiple uses. Store the source or a Supplier instead. The single-use contract is fundamental to the lazy, pull-based model.
What are common interview follow-up questions?
- "What exception is thrown?"
IllegalStateException. - "Can you reuse the source?" Yes:
list.stream()creates a new stream each time. - "What is a Supplier
?" A factory that creates a fresh, configured stream on each call.
Visual Overview
Stream<String> s = accounts.stream();
s.count() ← terminal: stream consumed
│
▼
Stream state: CONSUMED
│
s.forEach(...) ← reuse attempt
│
▼
IllegalStateException:
"stream has already been operated upon or closed"
Key Takeaway
A stream can be traversed only once. After any terminal operation consumes it, calling another operation on the same stream reference throws IllegalStateException. Streams are not reusable data structures but single-use, pull-based views over a source.
At the National Bank of India, a developer stored a stream in a variable and ran two terminals on it, hitting the exception on the second. The fix is to create a new stream from the source each time by calling list.stream() again, because the source collection is reusable even though the stream object is not.
When multiple traversals are needed, store the source or a Supplier that creates a fresh configured stream on each call. If the filtering or mapping is expensive, materialize the intermediate result into a collection and operate on that.
Remember one sentence: never reuse a stream after a terminal operation; create a new one from the source each time because streams are single-use.
---