In Kotlin, sequences let you process large collections lazily, which can reduce temporary allocations and improve performance for chained operations.
The problem with regular collections
Collection operations like map, filter, and flatMap are usually eager:
val result = users
.filter { it.isActive }
.map { it.email }
.take(10)
With a List, Kotlin typically creates intermediate collections:
- one list after
filter - another list after
map - then takes 10 items
For small collections, this is fine. For large collections, it can waste memory and CPU.
Use asSequence()
Convert the collection to a sequence:
val result = users
.asSequence()
.filter { it.isActive }
.map { it.email }
.take(10)
.toList()
Now each element flows through the pipeline one at a time:
user -> filter -> map -> maybe included in result
Also, because of take(10), processing can stop as soon as 10 matching items are found.
Example
data class User(
val name: String,
val email: String,
val isActive: Boolean
)
val emails = users
.asSequence()
.filter { it.isActive }
.map { it.email.lowercase() }
.distinct()
.take(100)
.toList()
This avoids building large intermediate lists before getting the final 100 emails.
When sequences help
Sequences are usually useful when you have:
- large collections
- multiple chained operations
- short-circuiting operations, such as:
takefirstfirstOrNullanyallnonefind
Example:
val firstAdminEmail = users
.asSequence()
.filter { it.isActive }
.filter { it.role == "admin" }
.map { it.email }
.firstOrNull()
This stops as soon as the first matching user is found.
When sequences may not help
Sequences are not always faster. They can be slower for:
- small collections
- simple one-step operations
- cases where you need the entire result anyway
- performance-critical tight loops where sequence overhead matters
For example, this may not benefit much:
val names = users.map { it.name }
A plain collection operation is often fine here.
Common pattern
Use this pattern:
val result = largeList
.asSequence()
.filter { condition(it) }
.map { transform(it) }
.take(50)
.toList()
The important parts are:
asSequence()starts lazy processing- intermediate operations stay lazy
- terminal operations execute the pipeline
Terminal operations include:
toList()
count()
first()
firstOrNull()
sumOf { ... }
any { ... }
forEach { ... }
Rule of thumb
Use sequences when your pipeline is large, chained, and potentially short-circuited.
For small or simple transformations, prefer normal collection operations for readability and often better performance.
