Back to Stream API Fundamentals
Q4Question 4 of 8

Can you reuse a stream after terminal operation?

Key Concepts
  • A stream CANNOT be reused after a terminal operation: any subsequent call throws IllegalStateException.
  • This is by design: streams are single-traversal pipelines backed by a Spliterator that cannot be rewound.
  • Supplier<Stream<T>> is the recommended workaround: a factory lambda that creates a fresh stream on each .get() call.
  • The collect-first approach materialises the filtered data into a List, then creates new streams from it for each operation.
  • Supplier is lazy and reflects source changes between calls; collect-first creates a snapshot.
  • Even calling an intermediate operation on a consumed stream throws IllegalStateException.
  • For simple cases like needing count and size, use the materialised List directly without creating a stream.

Definition

Streams are single-use: a terminal consumes the stream and any reuse throws IllegalStateException; the workaround is Supplier<Stream<T>> which creates a fresh stream on each .get() call.
  • 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?

JAVA
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());
        }
    }
}
TEXT
Count: 3
Error: stream has already been operated upon or closed

What is the Supplier workaround?

JAVA
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);
    }
}
TEXT
Count: 2
Total: Rs.375000

What is the collect-first alternative?

JAVA
// 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

DIAGRAM
  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>: a lambda that stores the pipeline definition and creates a fresh stream each time .get() is called. Each call produces an independent stream with its own Spliterator. The alternative is collect-first: materialise the data into a List, then create new streams from the List.

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.

---

Share
Use ← → arrow keys to navigate