Definition
- Single-use: after any terminal (collect, forEach, reduce, count, findFirst), the stream is consumed and marked as closed.
- IllegalStateException: any subsequent operation (even intermediate) on a consumed stream throws this exception.
- Spliterator cursor: the underlying cursor is exhausted after traversal and cannot be rewound.
- Supplier
> : a factory lambda that stores the pipeline definition and creates a fresh stream each time.get()is called. - Collect-first: materialise the data into a List with toList(), then create new streams from the List for each operation.
- Choice: Supplier for deferred execution and live data; collect-first for stable data that fits in memory.
A stream is like a one-time-use token at National Bank of India. Once Rajesh uses token number 42 at the counter (terminal operation), it is cancelled. He cannot present token 42 again. If he needs multiple turns at the counter, he needs a token machine (Supplier) that gives him a new token each time he presses the button.
Understanding the Concepts
Can you reuse a stream after a terminal operation?
No. After any terminal operation, the stream is consumed and throws IllegalStateException on any subsequent use.
What happens when you try to reuse?
package com.bank.core;
import java.util.stream.Stream;
public class StreamReuseError {
public static void main(String[] args) {
Stream<String> stream = Stream.of("SAV2001", "SAV2002", "CUR3001");
long count = stream.count(); // terminal #1: consumed
System.out.println("Count: " + count);
try {
stream.toList(); // THROWS!
} catch (IllegalStateException e) {
System.out.println("Error: " + e.getMessage());
}
}
}
Count: 3
Error: stream has already been operated upon or closed
What is the Supplier workaround?
package com.bank.service;
import java.math.BigDecimal;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Stream;
public class SupplierWorkaround {
record Account(String id, BigDecimal balance, boolean active) {}
public static void main(String[] args) {
List<Account> accounts = List.of(
new Account("SAV2001", new BigDecimal("125000"), true),
new Account("SAV2002", new BigDecimal("8000"), false),
new Account("CUR3001", new BigDecimal("250000"), true)
);
// Store the RECIPE, not the stream:
Supplier<Stream<Account>> activeAccounts =
() -> accounts.stream().filter(Account::active);
// Each .get() creates a FRESH stream:
long count = activeAccounts.get().count();
List<Account> list = activeAccounts.get().toList();
BigDecimal total = activeAccounts.get()
.map(Account::balance)
.reduce(BigDecimal.ZERO, BigDecimal::add);
System.out.println("Count: " + count);
System.out.println("Total: Rs." + total);
}
}
Count: 2
Total: Rs.375000
What is the collect-first alternative?
// Materialise once:
List<Account> activeList = accounts.stream()
.filter(Account::active)
.toList();
// Reuse the List (not the stream):
long count = activeList.size();
BigDecimal total = activeList.stream()
.map(Account::balance)
.reduce(BigDecimal.ZERO, BigDecimal::add);
When should you use which?
| Approach | Source changes? | Memory | Lazy? |
|----------|----------------|--------|-------|
| Supplier | Reflects latest | Low (no copy) | Yes |
| Collect-first | Snapshot | Materialised list | No |
What should a senior engineer know?
The single-use nature of streams is a design choice that enables efficient resource management. File-backed streams (Files.lines) hold OS file handles that are released when the stream is consumed. Allowing reuse would require either keeping the handle open indefinitely or re-opening the file, both of which have problems. The Supplier pattern cleanly separates the pipeline definition from its execution.
Visual Overview
Stream lifecycle:
─────────────────
Stream<T> stream = source.stream().filter(...);
│
stream.count() ← terminal: CONSUMED
│
stream.toList() ← IllegalStateException!
Supplier workaround:
────────────────────
Supplier<Stream<T>> factory = () -> source.stream().filter(...);
│
factory.get().count() ← fresh stream #1
factory.get().toList() ← fresh stream #2
factory.get().reduce() ← fresh stream #3
(each .get() creates independent stream)
Key Takeaway
A stream cannot be reused after a terminal operation. Any subsequent call throws IllegalStateException because the internal Spliterator has been traversed and the stream is marked as consumed. This is by design: streams are single-traversal pipelines.
The recommended workaround is Supplier
Choose Supplier for deferred execution and live data. Choose collect-first for stable data that fits in memory.
Remember one sentence: streams are single-use, so store the recipe in a Supplier and call .get() for each fresh pipeline.
---