What you'll learn
Kotlin's collection operations are eager — great for readability, but they build an intermediate list at each step. For large data or long chains, a sequence processes elements lazily, one at a time, which is dramatically more efficient.
By the end you'll be able to:
- Explain eager vs lazy evaluation
- Convert a collection to a sequence
- Create sequences, including infinite ones
- Know when sequences are worth it
Eager collections vs lazy sequences
When you chain operations on a list, each one runs to completion and produces a whole new list before the next begins. A sequence flips this: each element travels through the entire chain before the next element starts, so no intermediate lists are built — and the sequence can stop early the moment it has what it needs:
Collection — eager
Each step processes the whole collection and builds a new intermediate list before the next step runs.
Sequence — lazy
Each element flows through the whole chain one at a time. No intermediate lists, and it can stop early.
// a collection pipeline is EAGER — each step builds a full intermediate list
val fromList = listOf(1, 2, 3, 4, 5)
.map { it * 2 } // builds [2, 4, 6, 8, 10]
.filter { it > 4 } // builds [6, 8, 10]
.first() // 6
// a sequence is LAZY — elements flow through one at a time, stopping early
val fromSeq = listOf(1, 2, 3, 4, 5).asSequence()
.map { it * 2 }
.filter { it > 4 }
.first() // stops as soon as it finds the first match
println(fromList) // 6
println(fromSeq) // 6Seeing laziness in action
The behaviour is easier to believe when you watch it. Adding printlns to the operations reveals that elements flow through one at a time, and processing stops as soon as first() is satisfied — it never even looks at 4 or 5:
val result = listOf(1, 2, 3, 4, 5).asSequence()
.map { println("map $it"); it * 2 }
.filter { println("filter $it"); it > 4 }
.first()
println("Result: $result")Key idea
map 1, filter 2, map 2, filter 4, then map 3, filter 6 — done. The eager list version would have printed all five maps, then all the filters. This early exit and lack of intermediate lists is why sequences shine on big data.Creating sequences
Convert any collection with .asSequence(). You can also build one from scratch with sequenceOf(...), or generate one — even an infinite one, which is only possible because sequences are lazy:
// an INFINITE sequence — fine, because it's lazy
val powers = generateSequence(1) { it * 2 } // 1, 2, 4, 8, ...
println(powers.take(5).toList()) // [1, 2, 4, 8, 16]Tip
first() or take(n)). For small lists, plain collection operations are simpler and just as fast — the sequence overhead isn't worth it.Recap & quick check
Key takeaways
- Collection operations are eager: each step builds a full intermediate list.
- Sequences are lazy: each element flows through the whole chain one at a time — no intermediate lists.
- Lazy evaluation enables early termination (first(), take(n) stop as soon as satisfied).
- Create sequences with asSequence(), sequenceOf(), or generateSequence() (which can be infinite).
- Use sequences for large data, long chains, or partial results; plain collections for small ones.
Quick check
1. How do collection operations evaluate?
2. How does a sequence process elements?
3. What key benefit does laziness enable?
4. Why can generateSequence create an infinite sequence?
5. When are sequences NOT worth it?
Great — you can process big data efficiently now. Next up: Module 22 — Generics.