What is the difference between a List and a Sequence in Kotlin, and when would you choose a Sequence for performance?
answer
- Eager = horizontal, new list per step
- Lazy = vertical, one element through whole chain
- asSequence() + terminal op
- Long chain + big data + short-circuit -> Sequence
- Small/single op -> eager wins
basics
~20 sA List runs each step fully and makes a new list every step. A Sequence is lazy: it pulls one element all the way through the chain. Use Sequences for long chains over large data.
solid answer
~40 sCollection operators (`map`, `filter`) on a `List` are eager: each call processes the whole collection and allocates a new intermediate `List`. A `Sequence` is lazy: nothing runs until a terminal operation (`toList`, `first`, `count`), then elements flow one at a time through the whole chain, so no intermediate lists are allocated. You convert with `.asSequence()`. Sequences win when you chain several operators over large data, or want short-circuiting (`first`, `take`, `find`) so you stop early. For small collections or a single operation, eager `List` ops are usually faster because Sequences add per-element iterator/lambda overhead. Rule of thumb: many steps + large/infinite data + early exit favors `Sequence`; small data or one step favors eager collection operators.
code
kotlin · 6 linesval nums = (1..1_000_000).toList()
val result = nums.asSequence()
.filter { it % 2 == 0 }
.map { it * it }
.take(5)
.toList() // terminal op triggers the lazy pipelinego deeper
Knows a Sequence is lazy and a List is eager, and can convert with asSequence().
Explains intermediate vs terminal ops and why long chains over large data favor Sequences.
Quantifies allocation and short-circuit savings and names cases where eager is faster.
Frames it as a profiling/measurement decision and warns against premature Sequence use on small data.
## The two execution models Kotlin's standard-library transformation operators come in two flavors that look identical but execute very differently. - **Eager (Iterable/Collection):** Operators like `map`, `filter`, `sorted` defined on `Iterable`/`List` are **horizontal**. Each operator processes the *entire* collection and returns a brand-new `List`. A chain of N operators allocates N intermediate lists. - **Lazy (Sequence):** The same-named operators defined on `Sequence` are **vertical** and lazy. They build a pipeline but do nothing until a **terminal operation** runs. Then each element is pulled through *every* step before the next element starts. No intermediate lists are created. ## Intermediate vs terminal operations - **Intermediate** operations (`map`, `filter`, `take`, `distinct`) return another `Sequence` and are lazy. - **Terminal** operations (`toList`, `first`, `count`, `sum`, `forEach`, `fold`) trigger the actual work and produce a non-sequence result. ## Why this matters for performance ```kotlin val list = (1..1_000_000).toList() // Eager: builds a 1M filtered list, then a 1M mapped list, then takes 10 val eager = list.filter { it % 2 == 0 }.map { it * 2 }.take(10) // Lazy: pulls elements until 10 pass; touches ~20 elements total val lazy = list.asSequence().filter { it % 2 == 0 }.map { it * 2 }.take(10).toList() ``` The eager version does ~2 million operations and allocates two big lists. The lazy version short-circuits after producing 10 results. ## When Sequences win - **Long chains:** Several `map`/`filter` steps — you avoid allocating an intermediate list per step. - **Large or infinite data:** `generateSequence` can model infinite streams; eager ops cannot. - **Short-circuiting:** `first`, `find`, `take`, `any`, `none` stop as soon as the answer is known. ## When eager collections win - **Small collections:** Iterator and lambda-dispatch overhead per element outweighs allocation savings. - **Single operation:** One `map` on a `List` is just a tight loop; wrapping in a `Sequence` adds indirection. - **Operations needing the whole dataset:** `sorted`, `groupingBy` buffer everything anyway, so laziness gives little. ## How to convert Use `list.asSequence()` to go lazy, and a terminal op (often `.toList()`) to materialize. Always end a `Sequence` chain with a terminal operation; an intermediate-only chain does nothing.
- What happens if you build a Sequence chain but never call a terminal operation?Nothing executes — intermediate operations are lazy, so the chain stores transformations but produces no result until a terminal op like toList/first/count runs.
- Does asSequence() copy the underlying list?No. It wraps the existing iterable with a lazy view; elements are read on demand, not copied.
Eager is an assembly line that finishes every station for all parts before moving on; a Sequence sends one part through every station, then the next.
saying these in an interview costs you the question
- Claiming Sequences are always faster than Lists
- Thinking map on a List is lazy
- Forgetting the terminal operation, expecting work to happen
- Believing asSequence() copies all elements upfront
- Confusing Sequence with Java parallel Stream