skip to content

Given the lazy element-at-a-time model, when does converting a List chain to asSequence() actually help, and when can it be neutral or slower? Justify from the model, not benchmarks.

level: seniorimportance: nice to knowfreq 35%

answer

  1. Helps: large input + long chain + short-circuit terminal
  2. Saves intermediate lists and skipped elements
  3. Costs: per-element iterator/lambda overhead, worse cache locality
  4. Single op or small list: eager is fine or faster
  5. sorted/distinct buffer and kill the streaming benefit

basics

~20 s

Laziness helps with large inputs, long chains, or when you need only part of the result (first/take): it skips intermediate lists and stops early. For tiny inputs or a single operator it adds overhead and can be neutral or slower.

solid answer

~50 s

The lazy model wins when (1) the input is **large** and the chain has **several** intermediate operators — you avoid allocating one full list per stage; (2) a **short-circuiting** terminal (`first`, `find`, `take(n)`, `any`) means you only need a prefix — element-at-a-time lets upstream stages stop pulling; (3) the source is **conceptually unbounded** or streamed. It's neutral or *slower* when (1) the input is **small** — the constant per-element iterator and lambda-dispatch overhead of the wrapper chain dominates and eager array-backed loops are cache-friendlier; (2) there's only **one** operator, so there's no intermediate list to save; (3) you need a **fully materialized** result anyway (`toList`) over the whole input with no short-circuit. The decision is structural: laziness pays for *avoided work* (skipped elements, skipped intermediate lists), and you must do enough work for those savings to beat the wrapper overhead.

code

kotlin · 8 lines
kotlin
// GOOD candidate for asSequence(): big input, multiple stages, short-circuit
val result = bigList.asSequence()
    .filter { it.active }
    .map { it.transform() }
    .first { it.isValid() }   // stops at first valid; no full lists built

// POOR candidate: small list, single op -> just use the eager form
val doubled = listOf(1, 2, 3).map { it * 2 }

go deeper

for a junior

Knows sequences help with big data and first/take, eager is fine for small lists.

for a middle

Names the two savings (intermediate lists, skipped elements) and the overhead cost.

for a senior

Reasons structurally about chain length, input size, short-circuiting, and stateful-operator caveats without leaning on benchmarks.

for a principal

Frames a team guideline and connects to JIT/cache behavior and measurement discipline, avoiding cargo-cult use of sequences.

## The cost/benefit ledger of laziness `asSequence()` doesn't make code faster by magic; it changes *what work happens*. Reason about it as a ledger. ### What laziness SAVES - **Intermediate list allocations.** Eager `filter→map→take` builds a full list per stage. Sequences build none. The more stages × the bigger the input, the bigger the saving. - **Skipped elements via short-circuiting.** Element-at-a-time means a terminal like `first()`, `find()`, `take(n)`, `any()` stops the pull early. Eager evaluation would have processed the entire collection at each stage first. ```kotlin // Huge win: scans only until the first match, no intermediate lists val firstBig = millionItems.asSequence() .map { expensive(it) } .filter { it.score > threshold } .first() ``` ### What laziness COSTS - **Per-element iterator overhead.** Each element is pulled through a chain of wrapper iterators (`next()`/`hasNext()` calls, megamorphic dispatch), versus tight eager loops over array-backed lists that the JIT and CPU cache handle very well. - **Lambda invocation per element, per stage** — same as eager, but without the locality benefits. ## When it HELPS - Large input **and** multiple intermediate operators. - Any short-circuiting terminal where you need only a prefix/first match. - Pipelines over unbounded/streamed sources where materializing is impossible. ## When it's NEUTRAL or SLOWER - **Small collections** (dozens/hundreds): wrapper overhead outweighs saved allocations. - **Single operator** (`list.map { }`): no intermediate list exists to save; eager is simpler and as fast. - **Full materialization with no short-circuit**: you touch every element at every stage either way, so you only trade allocations for iterator overhead — often a wash, sometimes worse. - **Stateful operators** (`sorted`, `distinct`) force buffering, erasing the streaming benefit. ## Rule of thumb Reach for `asSequence()` when the chain is long, the data is large, or a terminal lets you stop early. For short chains on small lists, plain collection operators are clearer and usually at least as fast.

  • Why can a single-operator sequence chain be slower than the eager equivalent?
    There's no intermediate list to save, so you only pay the sequence's per-element iterator/dispatch overhead with no offsetting benefit.
  • Does a terminal like sum() benefit from laziness on a full collection?
    Not much for allocation if there were no intermediate stages, and it can't short-circuit, so it mostly trades intermediate-list savings (if any) for iterator overhead.

saying these in an interview costs you the question

  • Claiming asSequence() is always faster than collection operators
  • Recommending sequences for tiny lists by default
  • Ignoring that short-circuiting is the main runtime win
  • Forgetting iterator/dispatch overhead exists
  • Assuming sorted on a sequence keeps the streaming advantage

context