skip to content

Can you call two terminal operations on the same Sequence instance? What happens, and how does this differ from calling a terminal op without one at all?

level: middleimportance: should knowfreq 45%

answer

  1. No terminal op = no execution
  2. Re-iterable sources re-run the chain each terminal
  3. Iterator.asSequence() is single-use -> throws on 2nd terminal
  4. No memoization between terminal calls
  5. Materialize with toList() to reuse a result

basics

~20 s

Some sequences can only be iterated once, so running a second terminal op on them throws. A sequence built from a collection can be re-run. And without any terminal op, the chain never executes at all.

solid answer

~40 s

Whether you can run a terminal op twice depends on how the sequence was created. Sequences backed by a re-iterable source (e.g. `list.asSequence()`, `sequenceOf(...)`) can be consumed multiple times — each terminal op re-runs the whole intermediate chain from the start. But **constrained/single-use** sequences — notably the one returned by `Iterator.asSequence()` or some `sequence { }` builders over a one-shot source — throw `IllegalStateException("This sequence can be consumed only once")` on the second terminal op, because the underlying iterator is already exhausted. Separately, with **no** terminal op the intermediate chain is never executed at all — a common bug. Key takeaways: a terminal op is required to run anything; intermediate ops are re-evaluated on each terminal call (no caching); and don't assume a sequence is replayable unless its source is.

code

kotlin · 7 lines
kotlin
val once = listOf(1, 2, 3).iterator().asSequence()
println(once.toList()) // [1, 2, 3]
println(once.toList()) // throws IllegalStateException: consumed only once

val many = listOf(1, 2, 3).asSequence()
println(many.toList()) // [1, 2, 3]
println(many.toList()) // [1, 2, 3] again — fine

go deeper

for a junior

Understands that a terminal op is needed and that calling toList twice usually works on a list-backed sequence.

for a middle

Distinguishes re-iterable vs single-use sources and knows the second terminal can throw IllegalStateException.

for a senior

Explains the iterator-factory model, lack of memoization, and the cost of multiple terminals on expensive chains.

for a principal

Designs sequence-returning APIs with explicit consumption semantics (materialize vs lazy) and documents single-use constraints to prevent misuse.

## Re-running terminals, single-use sequences, and the missing-terminal trap ### A terminal op is mandatory to execute anything Intermediate ops only build a lazy description. If you never call a terminal op, **zero** lambdas run: ```kotlin val s = sequenceOf(1, 2, 3).map { println(it); it } // prints NOTHING ``` Adding `s.toList()` is what triggers execution. Forgetting the terminal is a classic silent bug. ### Re-iterable vs constrained (single-use) sequences Sequences differ by **source**: - **Re-iterable**: `sequenceOf(...)`, `listOf(...).asSequence()`, `generateSequence { ... }`. Each terminal op restarts iteration from the beginning, re-running every intermediate lambda. There is **no memoization** — running `count()` then `toList()` evaluates the chain twice. - **Constrained / single-use**: created from a one-shot `Iterator`, e.g. `myIterator.asSequence()`, or a `sequence { }` block reading a one-shot source. The standard library wraps these so a second iteration throws: ```kotlin val once = listOf(1, 2, 3).iterator().asSequence() once.toList() // [1, 2, 3] once.toList() // IllegalStateException: This sequence can be consumed only once ``` ### Why the difference exists A `Sequence` is essentially a factory for an `Iterator`. A re-iterable source can hand out a fresh iterator each time; a single-use source has only one underlying iterator, which gets exhausted. The library guards re-use with `constrainOnce()`-style logic to fail fast instead of silently yielding empty results. ### No caching between terminals Because intermediate ops are re-evaluated per terminal call, expensive pipelines run multiple times if you call several terminals. If you need the materialized result more than once, call a terminal once (e.g. `toList()`) and reuse that list. ```kotlin val seq = (1..1_000_000).asSequence().map { expensive(it) } val a = seq.sum() // runs expensive() a million times val b = seq.count() // runs expensive() AGAIN a million times // Better: val materialized = seq.toList(); then sum()/count() on the list ``` ### Mental checklist - No terminal -> nothing executes. - Terminal re-runs the whole intermediate chain (no cache). - Single-use sequences throw on the 2nd terminal; re-iterable ones don't.

  • If you call sum() then count() on the same generateSequence pipeline with an expensive map, how many times does map run?
    Twice over the full sequence — once per terminal — because there is no caching; materialize with toList() first to avoid the recompute.
  • Which factory gives a single-use sequence?
    Iterator.asSequence() (and constrainOnce()); calling a terminal twice on it throws IllegalStateException.

saying these in an interview costs you the question

  • Assuming every sequence can be terminal-consumed repeatedly
  • Thinking intermediate results are cached between terminal calls
  • Not knowing Iterator.asSequence() is single-use
  • Believing a second terminal silently returns empty rather than throwing
  • Forgetting that no terminal means no execution

context