Sequences #
There are situations where a regular Collection isn’t efficient enough. Imagine you have a list with one million elements, then you apply filter, map, and take(10). With a Collection, Kotlin processes all one million elements in every operation — creating three intermediate lists, each containing hundreds of thousands of elements — only to finally take 10. Sequence flips this behavior around: instead of processing all elements per operation (eager), a Sequence processes one element fully from the start to the end of the pipeline before moving to the next element (lazy). The result: no intermediate collections, and processing stops once the need is satisfied. This article covers how Sequence works from the inside, when to use it, all the ways to create one, and the idiomatic patterns that optimize data processing in Kotlin.
Eager vs Lazy Evaluation #
The fundamental difference between Collection and Sequence is when operations are executed.
flowchart TD
subgraph Eager["Collection — Eager Evaluation"]
A1["[1,2,3,4,5,6,7,8,9,10]"] -->|"filter { it % 2 == 0 }"| B1["[2,4,6,8,10]\n(intermediate list 1)"]
B1 -->|"map { it * it }"| C1["[4,16,36,64,100]\n(intermediate list 2)"]
C1 -->|"take(3)"| D1["[4,16,36]"]
end
subgraph Lazy["Sequence — Lazy Evaluation"]
A2["1"] -->|filter| X2{even?}
X2 -->|yes: 2| B2["map → 4"] -->|take| R2["4 ✓"]
A2b["3"] -->|filter| X2b{even?}
X2b -->|no| Skip["skip"]
A2c["4"] -->|filter| X2c{even?}
X2c -->|yes: 4| B2c["map → 16"] -->|take| R2c["16 ✓"]
endval angka = (1..10).toList()
// Collection — eager: three operations, two intermediate lists
val hasilCollection = angka
.filter { it % 2 == 0 } // [2,4,6,8,10] — a new list
.map { it * it } // [4,16,36,64,100] — yet another new list
.take(3) // [4,16,36]
// Total elements processed: 10 + 5 + 5 = 20 operations
// Sequence — lazy: elements processed one by one until the need is satisfied
val hasilSequence = angka.asSequence()
.filter { it % 2 == 0 }
.map { it * it }
.take(3)
.toList() // terminal operation — only executed here
// Total elements processed: far fewer because it stops at the 6th element (number 6)
Sequences are lazy —filter,map, and other transformation operations are not executed until there’s a terminal operation liketoList(),first(),count(), orforEach(). If you forget to call a terminal operation, nothing happens.
Creating Sequences #
Kotlin provides several ways to create a Sequence, each suited to different situations.
asSequence — Converting from a Collection #
// The most common way: convert an existing Collection
val daftar = listOf(1, 2, 3, 4, 5)
val seq = daftar.asSequence()
// A Range becomes a Sequence directly
val seqRange = (1..1_000_000).asSequence()
// A String as a Sequence<Char>
val seqChar = "Kotlin".asSequence() // Sequence<Char>
sequenceOf — a Sequence from Direct Values #
// Like listOf, but produces a Sequence
val seq = sequenceOf(1, 2, 3, 4, 5)
val seqStr = sequenceOf("apel", "jeruk", "mangga")
// An empty Sequence
val kosong = emptySequence<Int>()
generateSequence — Infinite Sequences #
generateSequence is the way to create a Sequence whose values are generated dynamically — including infinite Sequences. Because it’s lazy, this is safe: elements are only created when requested.
// An infinite sequence — always add 1
val bilanganBulat = generateSequence(1) { it + 1 }
val seratus = bilanganBulat.take(100).toList()
// [1, 2, 3, ..., 100]
// An infinite Fibonacci sequence
val fibonacci = generateSequence(Pair(0L, 1L)) { (a, b) -> Pair(b, a + b) }
.map { it.first }
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
val fib20 = fibonacci.take(20).toList()
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181]
// A Fibonacci exceeding one million — take the last one
val fibBesarPertama = fibonacci.first { it > 1_000_000 }
// 1346269
// A bounded sequence — stops when the generator returns null
val pangkatDua = generateSequence(1) { if (it < 1024) it * 2 else null }
val hasilPangkat = pangkatDua.toList()
// [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
// A sequence from a file (line by line) — very efficient for large files
fun bacaBarisDemiBarisSequence(path: String): Sequence<String> =
generateSequence(java.io.BufferedReader(java.io.FileReader(path))::readLine)
The sequence Builder — Full Control with yield #
The sequence { } builder lets you produce values one by one using yield or yieldAll. This is the most flexible way to create custom Sequences.
// A Sequence with yield — values generated on demand
val genap = sequence {
var n = 0
while (true) {
yield(n) // produce a value, then suspend until the next one is requested
n += 2
}
}
val genap10 = genap.take(5).toList() // [0, 2, 4, 6, 8]
// yieldAll — produce all elements from another collection or sequence
val campuran = sequence {
yield(0)
yieldAll(listOf(1, 2, 3))
yieldAll(generateSequence(4) { it + 1 }.take(3))
yield(100)
}
campuran.toList() // [0, 1, 2, 3, 4, 5, 6, 100]
// A tree sequence — lazy tree traversal
data class Node(val nilai: Int, val kiri: Node? = null, val kanan: Node? = null)
fun Node.traversalInOrder(): Sequence<Int> = sequence {
kiri?.let { yieldAll(it.traversalInOrder()) }
yield(nilai)
kanan?.let { yieldAll(it.traversalInOrder()) }
}
val pohon = Node(4,
Node(2, Node(1), Node(3)),
Node(6, Node(5), Node(7))
)
pohon.traversalInOrder().toList() // [1, 2, 3, 4, 5, 6, 7]
Intermediate and Terminal Operations #
Operations on a Sequence are divided into two: intermediate (returning a new Sequence, lazy) and terminal (triggering execution, returning a value/collection).
flowchart LR
A["Sequence"] --> B["Intermediate Operations\n(lazy — not yet executed)"]
B --> B1["filter { }"]
B --> B2["map { }"]
B --> B3["flatMap { }"]
B --> B4["take(n)"]
B --> B5["drop(n)"]
B --> B6["distinct()"]
B --> B7["sorted()"]
B --> B8["onEach { }"]
B --> C["Terminal Operations\n(eager — trigger execution)"]
C --> C1["toList()"]
C --> C2["toSet()"]
C --> C3["first() / last()"]
C --> C4["count()"]
C --> C5["sum() / sumOf { }"]
C --> C6["forEach { }"]
C --> C7["any { } / all { } / none { }"]
C --> C8["find { }"]Intermediate Operations #
val seq = (1..10).asSequence()
// filter, map, flatMap — the same as Collection but lazy
val genap = seq.filter { it % 2 == 0 } // Sequence<Int>
val kuadrat = seq.map { it.toDouble().pow(2) } // Sequence<Double>
val datar = seq.flatMap { listOf(it, it * 10) } // Sequence<Int>
// take and drop
val lima = seq.take(5) // take the first 5
val buang3 = seq.drop(3) // skip the first 3
// takeWhile and dropWhile — stop/start based on a condition
val kecilDari5 = seq.takeWhile { it < 5 } // 1, 2, 3, 4
val ab5Keatas = seq.dropWhile { it < 5 } // 5, 6, 7, 8, 9, 10
// distinct — remove duplicates lazily
val denganDuplikat = sequenceOf(1, 2, 2, 3, 3, 3, 4)
val unik = denganDuplikat.distinct() // 1, 2, 3, 4
// onEach — a side effect without changing the sequence (useful for debugging)
val denganLog = seq
.filter { it % 2 == 0 }
.onEach { println("After filter: $it") } // only logging
.map { it * it }
.onEach { println("After map: $it") }
// zip — combine two sequences
val a = sequenceOf(1, 2, 3)
val b = sequenceOf("satu", "dua", "tiga")
val digabung = a.zip(b) // Sequence<Pair<Int, String>>
// (1, "satu"), (2, "dua"), (3, "tiga")
// chunked and windowed — the same as Collection
val chunks = (1..10).asSequence().chunked(3)
// [1,2,3], [4,5,6], [7,8,9], [10]
Terminal Operations #
val seq = (1..1_000_000).asSequence()
// Collecting results
val list = seq.filter { it % 2 == 0 }.take(5).toList() // [2, 4, 6, 8, 10]
val set = seq.take(5).toSet() // {1, 2, 3, 4, 5}
// First/last elements
val pertama = seq.filter { it > 999_990 }.first() // 999991 — very efficient
val pertamaOrNull = seq.filter { it > 2_000_000 }.firstOrNull() // null
// Searching
val ditemukan = seq.find { it % 7 == 0 && it > 100 } // 105
val ada = seq.any { it > 999_999 } // true — stops at 1000000
val semua = seq.all { it > 0 } // true
val tidakAda = seq.none { it > 2_000_000 } // true
// Aggregation
val total = (1..100).asSequence().sum() // 5050
val rata = (1..10).asSequence().average() // 5.5
val count = seq.filter { it % 1000 == 0 }.count() // 1000
// forEach
(1..5).asSequence()
.map { it * it }
.forEach { println(it) } // 1, 4, 9, 16, 25
Performance: Sequence vs Collection #
Sequences are beneficial in certain conditions — but not always faster.
When Sequence Is Faster #
import kotlin.system.measureTimeMillis
val data = (1..1_000_000).toList()
// Collection: creates an intermediate list for every operation
val waktuCollection = measureTimeMillis {
val hasil = data
.filter { it % 2 == 0 } // new list: 500,000 elements
.map { it.toLong() * it } // new list: 500,000 elements
.filter { it > 1_000_000 } // new list: most of them
.take(10) // take 10
}
// Sequence: no intermediate lists, stops after 10 elements are satisfied
val waktuSequence = measureTimeMillis {
val hasil = data.asSequence()
.filter { it % 2 == 0 }
.map { it.toLong() * it }
.filter { it > 1_000_000 }
.take(10)
.toList()
}
// Sequence is far faster here because:
// 1. No intermediate lists are created (saves memory)
// 2. Processing stops after 10 elements are satisfied (saves CPU)
When Collection Is Faster #
// For SMALL collections, Sequence overhead outweighs the benefits
val kecil = listOf(1, 2, 3, 4, 5)
// Collection: direct, no overhead
kecil.filter { it > 2 }.map { it * 2 } // [6, 8, 10]
// Sequence: there's a coroutine suspension mechanism overhead
kecil.asSequence().filter { it > 2 }.map { it * 2 }.toList()
// Slower for small lists because of the Sequence setup overhead
// Operations requiring all elements — no lazy benefit
val sorted = kecil.asSequence().sorted().toList() // must see all elements first
A Selection Guide #
flowchart TD
A{Data size?} --> B["Small\n< ~1,000 elements"]
A --> C["Large\n> ~1,000 elements"]
B --> D["Use a Collection\nThe Sequence overhead isn't worth it"]
C --> E{Has take/first/find\noperations?}
E -- Yes --> F["Use a Sequence\nCan stop early"]
E -- No --> G{Many chained\noperations?}
G -- Yes --> H["Consider a Sequence\nSaves intermediate lists"]
G -- No --> I["Collection is enough"]Use a Collection if:
✓ Data is small (< 1,000 elements)
✓ You need random access (by index)
✓ Operations need all elements first (sorted, groupBy)
✓ A single operation without chaining
Use a Sequence if:
✓ Data is large (> 10,000 elements)
✓ A long pipeline with many chained operations
✓ There are take(), first(), find() — can stop early
✓ Infinite sequences (generateSequence)
✓ Data streams that don't need to be loaded all into memory
✓ Reading large files line by line
Infinite Sequences — Advanced Patterns #
One of the biggest Sequence advantages is the ability to represent infinite data safely because it’s lazy.
// Prime numbers — an infinite sequence
val prima = sequence {
var kandidat = 2
val ditemukan = mutableListOf<Int>()
while (true) {
if (ditemukan.none { kandidat % it == 0 }) {
ditemukan.add(kandidat)
yield(kandidat)
}
kandidat++
}
}
val prima10 = prima.take(10).toList()
// [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
val primaKurangDari100 = prima.takeWhile { it < 100 }.toList()
// [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
// The Collatz sequence for a number n
fun collatz(n: Long): Sequence<Long> = generateSequence(n) { nilai ->
when {
nilai == 1L -> null // stop at 1
nilai % 2 == 0L -> nilai / 2
else -> nilai * 3 + 1
}
}
collatz(27).toList() // 27, 82, 41, 124, 62, 31, 94, ... (ends at 1)
collatz(27).count() // 112 (the sequence length)
// A power of two sequence
val pangkatDua = generateSequence(1L) { it * 2 }
val sampai1Miliar = pangkatDua.takeWhile { it <= 1_000_000_000 }.toList()
// [1, 2, 4, 8, 16, ..., 536870912]
// A Sequence with complex state
data class KondisiSimulasi(val posisi: Double, val kecepatan: Double)
fun simulasiFisika(kondisiAwal: KondisiSimulasi, dt: Double): Sequence<KondisiSimulasi> =
generateSequence(kondisiAwal) { kondisi ->
val percepatanGravitasi = -9.81
val kecepatanBaru = kondisi.kecepatan + percepatanGravitasi * dt
val posisiBaru = kondisi.posisi + kecepatanBaru * dt
if (posisiBaru < 0) null // stop when it touches the ground
else KondisiSimulasi(posisiBaru, kecepatanBaru)
}
val lintasan = simulasiFisika(KondisiSimulasi(100.0, 0.0), dt = 0.1)
val titikTertinggi = lintasan.maxByOrNull { it.posisi }
val waktuJatuh = lintasan.count() * 0.1
Sequences for File Processing #
Sequences are very efficient for reading and processing large files because they don’t load the entire file into memory.
import java.io.File
// Reading a large file line by line — constant memory
fun prosesFileBesar(path: String): Map<String, Int> {
return File(path)
.useLines { baris -> // useLines manages resources automatically
baris
.filter { it.isNotBlank() }
.flatMap { it.split(" ").asSequence() }
.groupingBy { it.lowercase() }
.eachCount()
}
}
// useLines is more idiomatic than readLines for large files
// readLines() loads all lines into a List — not suitable for large files
// Count lines meeting a condition without loading everything into memory
fun hitungBarisKondisi(path: String, kondisi: (String) -> Boolean): Int {
return File(path).useLines { baris ->
baris.count(kondisi)
}
}
// Take the first N lines meeting a condition
fun ambilBarisKondisi(path: String, n: Int, kondisi: (String) -> Boolean): List<String> {
return File(path).useLines { baris ->
baris.filter(kondisi).take(n).toList()
}
}
onEach — Debugging Sequences #
onEach is an intermediate operation useful for seeing what happens inside a pipeline without changing its flow.
val hasil = (1..10)
.asSequence()
.onEach { println("Input: $it") }
.filter { it % 2 == 0 }
.onEach { println("After filter: $it") }
.map { it * it }
.onEach { println("After map: $it") }
.take(3)
.toList()
// The output shows elements are processed one by one:
// Input: 1
// Input: 2 ← 2 passes the filter
// After filter: 2
// After map: 4
// Input: 3
// Input: 4 ← 4 passes the filter
// After filter: 4
// After map: 16
// Input: 5
// Input: 6 ← 6 passes the filter
// After filter: 6
// After map: 36
// (stops because take(3) is satisfied — 7, 8, 9, 10 aren't processed)
println(hasil) // [4, 16, 36]
Converting Sequences to Collections #
After the pipeline operations finish, a Sequence needs to be converted to a Collection for further use.
val seq = (1..10).asSequence().filter { it % 2 == 0 }
// Convert to various Collections
val list: List<Int> = seq.toList()
val set: Set<Int> = seq.toSet()
val mutableList: MutableList<Int> = seq.toMutableList()
// To a Map
val angka = (1..5).asSequence()
val segiEmpat = angka.associateWith { it * it }
// {1=1, 2=4, 3=9, 4=16, 5=25}
// joinToString — directly to a String without an intermediate List
val hasil = (1..5).asSequence()
.map { it * it }
.joinToString(", ") // "1, 4, 9, 16, 25"
Summary #
- Lazy evaluation is the key difference between Sequence and Collection — intermediate operations (
filter,map, etc.) aren’t executed until there’s a terminal operation (toList(),first(),forEach(), etc.).- No intermediate lists — a Sequence processes elements one by one through the entire pipeline, significantly saving memory for large data.
- Stopping early — operations like
take(n),first { }, andfind { }stop processing once the need is satisfied. This is a Sequence’s biggest advantage for pipelines with tight filters.generateSequencefor Sequences whose values are generated dynamically, including infinite Sequences. The generator returnsnullto stop the Sequence.- The
sequence { yield() }builder for custom Sequences with complex logic — can use conditions, loops, and internal state.yieldAllinserts an entire collection/sequence.- Use Sequences for large data (> ~1,000 elements) with long pipelines. For small data, the Sequence overhead outweighs the benefits — stick with Collections.
onEachfor debugging pipelines without changing the flow — see element values at each pipeline stage.File.useLines { }is the idiomatic pattern for reading large files line by line with constant memory — far more efficient thanreadLines()which loads all lines into a List.- Sequences can’t be reused — after a terminal operation is called, the Sequence is exhausted. Create a new Sequence for a second iteration.
- sorted(), groupBy(), and operations needing all elements don’t benefit from Sequence laziness — these still need all elements before they can continue.