Sequences, lazy execution, and performance
A collection pipeline normally completes one operation for the complete input before starting the next. A sequence normally moves one element through the pipeline at a time.
The same pipeline, two execution orders
Eager collection
fun main() {
val result = (1..5)
.filter {
println("filter $it")
it % 2 == 0
}
.map {
println("map $it")
it * 10
}
.take(1)
println("result=$result")
}
FILTER all: 1 2 3 4 5 -> [2, 4]
MAP all: -> [20, 40]
TAKE one: -> [20]
The final result needs one value, but filter examined all five inputs and map transformed both matches.
Lazy sequence
fun main() {
val result = (1..5)
.asSequence()
.filter {
println("filter $it")
it % 2 == 0
}
.map {
println("map $it")
it * 10
}
.take(1)
.toList()
println("result=$result")
}
1 -> filter fails
2 -> filter passes -> map to 20 -> take accepts -> STOP
The sequence does not inspect 3, 4, or 5 because the terminal result already has one item.
Intermediate and terminal operations
Operations such as filter and map configure a sequence pipeline. Nothing runs until a terminal operation requests values.
fun main() {
val pipeline = sequenceOf(1, 2, 3).map {
println("mapping $it")
it * 10
}
println("pipeline created")
println(pipeline.toList())
}
toList, first, count, any, and iteration are terminal consumers. Reusing a reusable sequence can repeat upstream computation. Some constrained-once sequences cannot be consumed twice.
When sequences can help
- Large inputs with multiple transformations
- Early termination with
take,first, orany - Avoiding intermediate collection allocations
- Potentially unbounded/generated input
When they may hurt
- Tiny inputs and one simple operation
- Pipelines that must process every element anyway
- Tight loops where iterator/lambda overhead costs more than saved allocations
- Code where a materialized snapshot makes ownership/debugging clearer
A sequence runs synchronously on the thread that consumes it. It is not a coroutine, Flow, background worker, or parallel stream. Calling asSequence() on the main thread does not make expensive work safe for the UI.
Side effects and repeated consumption
fun main() {
val source = sequenceOf(1, 2, 3).onEach { println("seen $it") }
println(source.take(1).toList())
println(source.take(2).toList())
}
Both terminal operations execute the pipeline. Side effects inside lazy operations can therefore repeat or occur only for the portion consumed.
Android scenario
A sequence can reduce temporary allocations while mapping a large cached list into the first few matching UI models. It does not solve loading the entire database into memory; Paging or a filtered database query may be the correct architectural boundary.
Interview questions
- Why can a sequence stop earlier than a collection chain?
- Does declaring a sequence execute its
mapimmediately? - Is Sequence asynchronous or thread-switching?
- Why can Sequence be slower for a tiny list?
- What happens when a pipeline containing side effects is consumed twice?