skip to content

Stream Pitfalls

The recurring stream mistakes: side effects in intermediate operations, mutating the source mid-pipeline, reusing a consumed stream, stateful lambdas that break under parallelism, and toMap blowing up on duplicate keys. Interviewers often present code containing one and ask what is wrong.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

6

What happens if you operate on a Java stream after it has already been consumed, and why is a stream single-use?

level: juniorimportance: must knowfreq 60%

answer

  1. Stream = single traversal, like a one-shot iterator
  2. 'stream has already been operated upon or closed' = IllegalStateException
  3. Re-call source.stream() for a fresh pipeline
  4. Supplier<Stream<T>> to reuse the recipe
  5. Lazy + one-pass sources (files, generators) = no rewind

basics

~10 s

A stream can only be used once. After a terminal operation like collect() or forEach() runs, the stream is consumed, and touching it again throws IllegalStateException. Create a fresh stream from the source instead.

solid answer

~50 s

A Java stream is a single-use pipeline, not a reusable collection. Once a terminal operation (collect, forEach, count, reduce, findFirst, etc.) runs, the stream is marked consumed. Any further operation — even an intermediate one like filter — throws IllegalStateException: 'stream has already been operated upon or closed.' This catches people who store a Stream in a variable and call it twice, or who pass a stream around as if it were a List. The design reason is that streams may be lazy and may not buffer their elements; the source can be a one-pass thing like a file or a generator, so the stream cannot rewind. The fix is to operate on the source: keep the collection (or a Supplier<Stream<T>>) and call .stream() again each time you need a fresh pipeline. If you genuinely need the result twice, collect it once into a List and reuse that.

go deeper

for a junior

Knows a stream is single-use and that reusing it throws IllegalStateException; recreates with source.stream().

for a middle

Distinguishes intermediate vs terminal ops, explains the laziness/one-pass rationale, and uses Supplier<Stream<T>>.

for a senior

Explains why one-pass semantics enable fused, low-memory execution and one-shot sources, and designs APIs to pass collections or suppliers rather than live streams.

for a principal

Sets conventions: never expose Stream as a return type to be reused; prefer returning a List or a Supplier; understands the interaction with closeable streams (Files.lines) and resource leaks.

## Terms - **Stream**: a pipeline over a sequence of elements. It is *not* a data structure that stores elements; it is a description of computation pulled from a **source** (a collection, an array, a file, a generator). - **Intermediate operation**: an operation that returns another stream and is **lazy** — `filter`, `map`, `sorted`, `peek`, `distinct`. It does nothing until a terminal op runs. - **Terminal operation**: an operation that produces a result or side effect and **triggers execution** — `collect`, `forEach`, `count`, `reduce`, `findFirst`, `anyMatch`, `toList`. After it runs, the pipeline is done. ## The rule A stream can be **traversed only once**. After a terminal operation completes (or the stream is closed), the stream instance is spent. Calling *any* operation on it again throws: ``` java.lang.IllegalStateException: stream has already been operated upon or closed ``` Example of the bug: ```java Stream<String> s = names.stream(); long count = s.count(); // terminal — consumes the stream List<String> list = s.collect(toList()); // BOOM: IllegalStateException ``` Even this throws, because each operation is still on the *same* consumed stream: ```java Stream<Integer> s = nums.stream(); Stream<Integer> a = s.filter(x -> x > 0); Stream<Integer> b = s.filter(x -> x < 0); // BOOM: s already operated upon ``` ## Why streams are single-use Streams are designed to be **lazy** and to support sources that can only be read **once** — a network socket, a `BufferedReader.lines()`, an `IntStream.generate(...)`. If a stream had to support re-traversal, it would have to buffer every element it ever saw, defeating the whole point (low memory, fused single-pass execution). So the API chooses one-pass semantics and fails fast if you violate them. ## How to do it right 1. **Re-derive from the source.** The collection is reusable; the stream is not: ```java long count = names.stream().count(); List<String> list = names.stream().collect(toList()); // fresh stream ``` 2. **Use a `Supplier<Stream<T>>`** when you must pass "a stream" around and use it multiple times: ```java Supplier<Stream<String>> sup = () -> names.stream(); sup.get().count(); sup.get().collect(toList()); ``` 3. **Collect once, reuse the result.** If the computation is expensive, run the pipeline once into a `List` and operate on the list afterward. ## Key mental model Treat a `Stream` like a one-shot iterator, not like a `List`. A `List` is a value you can read repeatedly; a `Stream` is an in-flight computation you consume exactly once.

  • How can you 'reuse' a stream pipeline when you truly need it more than once?
    Wrap the source in a Supplier<Stream<T>> and call get() each time, or collect the result into a List once and operate on the List afterward.
  • Does calling only intermediate operations consume the stream?
    No. Intermediate operations are lazy and return a new stream; the original is consumed only once a terminal operation runs (or the stream is closed). But you still cannot branch the same stream into two pipelines.

saying these in an interview costs you the question

  • Treating a Stream like a List you can read twice
  • Storing a Stream in a field and reusing it across calls
  • Thinking the second operation 'continues where the first left off'
  • Believing intermediate ops are safe to call on a consumed stream

context

open as a page

Why does modifying the source collection while streaming it cause a ConcurrentModificationException, and how do you restructure to avoid it?

level: middleimportance: must knowfreq 52%

basics

~20 s

Streams read the source lazily through an iterator that detects changes. If you add or remove elements from the source collection while the stream is running, you usually get a ConcurrentModificationException. Build a new collection from the stream instead of editing the old one.

open as a page

What goes wrong when Collectors.toMap encounters duplicate keys, and how do you handle it correctly?

level: middleimportance: must knowfreq 58%

basics

~10 s

The two-argument Collectors.toMap throws IllegalStateException ('Duplicate key') when two elements produce the same key. Use the three-argument version with a merge function to decide which value wins.

open as a page

Why prefer IntStream/LongStream/DoubleStream over Stream<Integer> for numeric work, and what is the cost of getting it wrong?

level: juniorimportance: should knowfreq 55%

basics

~10 s

Stream<Integer> wraps every number in an Integer object (boxing), which wastes memory and time. IntStream keeps raw int values, so it is faster and has handy methods like sum() and average().

open as a page

Why are side effects in stream operations (and especially using peek for mutation) discouraged, and what is peek actually for?

level: middleimportance: should knowfreq 50%

basics

~20 s

Streams expect each step to just transform data, not change outside state. peek is meant for debugging/logging only. Mutating in peek or map is fragile because steps are lazy and may be skipped, reordered, or run in parallel.

open as a page

What happens when you build an infinite stream without a short-circuiting operation, and how do you bound it safely?

level: seniorimportance: should knowfreq 42%

basics

~20 s

An infinite stream (like Stream.iterate or Stream.generate) produces values forever. If you end it with a non-short-circuiting terminal op like collect or forEach, it never stops and the program hangs. Bound it with limit() or a takeWhile/short-circuiting op.

open as a page