Skip to main content

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")
}
Open in Kotlin Playground
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")
}
Open in Kotlin Playground
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())
}
Open in Kotlin Playground

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, or any
  • 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
Sequence is not asynchronous

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())
}
Open in Kotlin Playground

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​

  1. Why can a sequence stop earlier than a collection chain?
  2. Does declaring a sequence execute its map immediately?
  3. Is Sequence asynchronous or thread-switching?
  4. Why can Sequence be slower for a tiny list?
  5. What happens when a pipeline containing side effects is consumed twice?