Sequences #
Ada situasi di mana Collection biasa tidak cukup efisien. Bayangkan kamu punya list dengan satu juta elemen, lalu melakukan filter, map, dan take(10). Dengan Collection, Kotlin akan memproses seluruh satu juta elemen di setiap operasi — membuat tiga list perantara yang masing-masing berisi ratusan ribu elemen — hanya untuk akhirnya mengambil 10. Sequence membalik cara kerja ini: alih-alih memproses semua elemen per operasi (eager), Sequence memproses satu elemen sepenuhnya dari awal hingga akhir pipeline sebelum berpindah ke elemen berikutnya (lazy). Hasilnya: tidak ada collection perantara, dan pemrosesan berhenti begitu kebutuhan terpenuhi. Artikel ini membahas cara kerja Sequence dari dalam, kapan menggunakannya, semua cara membuatnya, dan pola idiomatik yang mengoptimalkan pemrosesan data di Kotlin.
Eager vs Lazy Evaluation #
Perbedaan mendasar antara Collection dan Sequence ada di kapan operasi dieksekusi.
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(list perantara 1)"]
B1 -->|"map { it * it }"| C1["[4,16,36,64,100]\n(list perantara 2)"]
C1 -->|"take(3)"| D1["[4,16,36]"]
end
subgraph Lazy["Sequence — Lazy Evaluation"]
A2["1"] -->|filter| X2{genap?}
X2 -->|ya: 2| B2["map → 4"] -->|take| R2["4 ✓"]
A2b["3"] -->|filter| X2b{genap?}
X2b -->|tidak| Skip["lewati"]
A2c["4"] -->|filter| X2c{genap?}
X2c -->|ya: 4| B2c["map → 16"] -->|take| R2c["16 ✓"]
endval angka = (1..10).toList()
// Collection — eager: tiga operasi, dua list perantara
val hasilCollection = angka
.filter { it % 2 == 0 } // [2,4,6,8,10] — list baru
.map { it * it } // [4,16,36,64,100] — list baru lagi
.take(3) // [4,16,36]
// Total elemen diproses: 10 + 5 + 5 = 20 operasi
// Sequence — lazy: elemen diproses satu per satu sampai kebutuhan terpenuhi
val hasilSequence = angka.asSequence()
.filter { it % 2 == 0 }
.map { it * it }
.take(3)
.toList() // terminal operation — baru dieksekusi di sini
// Total elemen diproses: jauh lebih sedikit karena berhenti di elemen ke-6 (angka 6)
Sequence bersifat lazy — operasifilter,map, dan transformasi lainnya tidak dieksekusi sampai ada terminal operation sepertitoList(),first(),count(), atauforEach(). Jika kamu lupa memanggil terminal operation, tidak ada yang terjadi.
Membuat Sequence #
Kotlin menyediakan beberapa cara membuat Sequence, masing-masing cocok untuk situasi berbeda.
asSequence — Konversi dari Collection #
// Cara paling umum: konversi Collection yang sudah ada
val daftar = listOf(1, 2, 3, 4, 5)
val seq = daftar.asSequence()
// Range langsung menjadi Sequence
val seqRange = (1..1_000_000).asSequence()
// String sebagai Sequence<Char>
val seqChar = "Kotlin".asSequence() // Sequence<Char>
sequenceOf — Sequence dari Nilai Langsung #
// Seperti listOf, tapi menghasilkan Sequence
val seq = sequenceOf(1, 2, 3, 4, 5)
val seqStr = sequenceOf("apel", "jeruk", "mangga")
// Sequence kosong
val kosong = emptySequence<Int>()
generateSequence — Sequence Tak Terbatas #
generateSequence adalah cara membuat Sequence yang nilainya digenerate secara dinamis — termasuk Sequence tak terbatas. Karena lazy, ini aman: elemen hanya dibuat saat diminta.
// Sequence tak terbatas — selalu tambah 1
val bilanganBulat = generateSequence(1) { it + 1 }
val seratus = bilanganBulat.take(100).toList()
// [1, 2, 3, ..., 100]
// Sequence Fibonacci — tak terbatas
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]
// Fibonacci yang melampaui satu juta — ambil yang terakhir
val fibBesarPertama = fibonacci.first { it > 1_000_000 }
// 1346269
// Sequence terbatas — berhenti saat generator mengembalikan 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]
// Sequence dari file (baris per baris) — sangat efisien untuk file besar
fun bacaBarisDemiBarisSequence(path: String): Sequence<String> =
generateSequence(java.io.BufferedReader(java.io.FileReader(path))::readLine)
sequence Builder — Kontrol Penuh dengan yield #
sequence { } builder memungkinkan menghasilkan nilai satu per satu menggunakan yield atau yieldAll. Ini adalah cara paling fleksibel untuk membuat Sequence kustom.
// Sequence dengan yield — nilai digenerate on demand
val genap = sequence {
var n = 0
while (true) {
yield(n) // hasilkan nilai, lalu suspend sampai nilai berikutnya diminta
n += 2
}
}
val genap10 = genap.take(5).toList() // [0, 2, 4, 6, 8]
// yieldAll — hasilkan semua elemen dari collection atau sequence lain
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]
// Sequence pohon — traversal tree secara lazy
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]
Operasi Intermediate dan Terminal #
Operasi pada Sequence dibagi dua: intermediate (mengembalikan Sequence baru, lazy) dan terminal (memicu eksekusi, mengembalikan nilai/collection).
flowchart LR
A["Sequence"] --> B["Intermediate Operations\n(lazy — belum dieksekusi)"]
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 — memicu eksekusi)"]
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 — sama seperti Collection tapi 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 dan drop
val lima = seq.take(5) // ambil 5 pertama
val buang3 = seq.drop(3) // lewati 3 pertama
// takeWhile dan dropWhile — berhenti/mulai berdasarkan kondisi
val kecilDari5 = seq.takeWhile { it < 5 } // 1, 2, 3, 4
val ab5Keatas = seq.dropWhile { it < 5 } // 5, 6, 7, 8, 9, 10
// distinct — hapus duplikat secara lazy
val denganDuplikat = sequenceOf(1, 2, 2, 3, 3, 3, 4)
val unik = denganDuplikat.distinct() // 1, 2, 3, 4
// onEach — efek samping tanpa mengubah sequence (berguna untuk debugging)
val denganLog = seq
.filter { it % 2 == 0 }
.onEach { println("Setelah filter: $it") } // hanya logging
.map { it * it }
.onEach { println("Setelah map: $it") }
// zip — gabungkan dua sequence
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 dan windowed — sama seperti 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()
// Mengumpulkan hasil
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}
// Elemen pertama/terakhir
val pertama = seq.filter { it > 999_990 }.first() // 999991 — sangat efisien
val pertamaOrNull = seq.filter { it > 2_000_000 }.firstOrNull() // null
// Pencarian
val ditemukan = seq.find { it % 7 == 0 && it > 100 } // 105
val ada = seq.any { it > 999_999 } // true — berhenti di 1000000
val semua = seq.all { it > 0 } // true
val tidakAda = seq.none { it > 2_000_000 } // true
// Agregasi
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
Performa: Sequence vs Collection #
Sequence menguntungkan dalam kondisi tertentu — tapi bukan selalu lebih cepat.
Kapan Sequence Lebih Cepat #
import kotlin.system.measureTimeMillis
val data = (1..1_000_000).toList()
// Collection: membuat list perantara di setiap operasi
val waktuCollection = measureTimeMillis {
val hasil = data
.filter { it % 2 == 0 } // list baru: 500.000 elemen
.map { it.toLong() * it } // list baru: 500.000 elemen
.filter { it > 1_000_000 } // list baru: sebagian besar
.take(10) // ambil 10
}
// Sequence: tidak ada list perantara, berhenti setelah 10 elemen terpenuhi
val waktuSequence = measureTimeMillis {
val hasil = data.asSequence()
.filter { it % 2 == 0 }
.map { it.toLong() * it }
.filter { it > 1_000_000 }
.take(10)
.toList()
}
// Sequence jauh lebih cepat di sini karena:
// 1. Tidak membuat list perantara (hemat memori)
// 2. Berhenti memproses setelah 10 elemen terpenuhi (hemat CPU)
Kapan Collection Lebih Cepat #
// Untuk collection KECIL, overhead Sequence lebih besar dari manfaatnya
val kecil = listOf(1, 2, 3, 4, 5)
// Collection: langsung, tidak ada overhead
kecil.filter { it > 2 }.map { it * 2 } // [6, 8, 10]
// Sequence: ada overhead coroutine suspension mechanism
kecil.asSequence().filter { it > 2 }.map { it * 2 }.toList()
// Lebih lambat untuk list kecil karena overhead setup Sequence
// Operasi yang membutuhkan seluruh elemen — tidak ada keuntungan lazy
val sorted = kecil.asSequence().sorted().toList() // harus lihat semua elemen dulu
Panduan Memilih #
flowchart TD
A{Ukuran data?} --> B["Kecil\n< ~1.000 elemen"]
A --> C["Besar\n> ~1.000 elemen"]
B --> D["Gunakan Collection\nOverhead Sequence tidak worth it"]
C --> E{Ada operasi\ntake/first/find?}
E -- Ya --> F["Gunakan Sequence\nBisa berhenti lebih awal"]
E -- Tidak --> G{Banyak operasi\nberantai?}
G -- Ya --> H["Pertimbangkan Sequence\nHemat list perantara"]
G -- Tidak --> I["Collection cukup"]Gunakan Collection jika:
✓ Data kecil (< 1.000 elemen)
✓ Butuh akses acak (by index)
✓ Operasi yang butuh seluruh elemen lebih dulu (sorted, groupBy)
✓ Operasi tunggal tanpa chaining
Gunakan Sequence jika:
✓ Data besar (> 10.000 elemen)
✓ Pipeline panjang dengan banyak operasi berantai
✓ Ada take(), first(), find() — bisa berhenti lebih awal
✓ Sequence tak terbatas (generateSequence)
✓ Stream data yang tidak perlu dimuat semua ke memori
✓ Membaca file besar baris per baris
Sequence Tak Terbatas — Pola Lanjutan #
Salah satu keunggulan terbesar Sequence adalah kemampuan merepresentasikan data tak terbatas yang aman karena lazy.
// Bilangan prima — sequence tak terbatas
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]
// Collatz sequence untuk angka n
fun collatz(n: Long): Sequence<Long> = generateSequence(n) { nilai ->
when {
nilai == 1L -> null // berhenti di 1
nilai % 2 == 0L -> nilai / 2
else -> nilai * 3 + 1
}
}
collatz(27).toList() // 27, 82, 41, 124, 62, 31, 94, ... (berakhir di 1)
collatz(27).count() // 112 (panjang sequence)
// 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]
// Sequence dengan state yang kompleks
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 // berhenti saat menyentuh tanah
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
Sequence untuk Pemrosesan File #
Sequence sangat efisien untuk membaca dan memproses file besar karena tidak memuat seluruh file ke memori.
import java.io.File
// Membaca file besar baris per baris — memori konstan
fun prosesFileBesar(path: String): Map<String, Int> {
return File(path)
.useLines { baris -> // useLines mengelola resource otomatis
baris
.filter { it.isNotBlank() }
.flatMap { it.split(" ").asSequence() }
.groupingBy { it.lowercase() }
.eachCount()
}
}
// useLines lebih idiomatik dari readLines untuk file besar
// readLines() memuat semua baris ke List — tidak cocok untuk file besar
// Hitung baris yang memenuhi kondisi tanpa load semua ke memori
fun hitungBarisKondisi(path: String, kondisi: (String) -> Boolean): Int {
return File(path).useLines { baris ->
baris.count(kondisi)
}
}
// Ambil N baris pertama yang memenuhi kondisi
fun ambilBarisKondisi(path: String, n: Int, kondisi: (String) -> Boolean): List<String> {
return File(path).useLines { baris ->
baris.filter(kondisi).take(n).toList()
}
}
onEach — Debugging Sequence #
onEach adalah intermediate operation yang berguna untuk melihat apa yang terjadi di dalam pipeline tanpa mengubah alurnya.
val hasil = (1..10)
.asSequence()
.onEach { println("Input: $it") }
.filter { it % 2 == 0 }
.onEach { println("Setelah filter: $it") }
.map { it * it }
.onEach { println("Setelah map: $it") }
.take(3)
.toList()
// Output memperlihatkan bahwa elemen diproses satu per satu:
// Input: 1
// Input: 2 ← 2 lolos filter
// Setelah filter: 2
// Setelah map: 4
// Input: 3
// Input: 4 ← 4 lolos filter
// Setelah filter: 4
// Setelah map: 16
// Input: 5
// Input: 6 ← 6 lolos filter
// Setelah filter: 6
// Setelah map: 36
// (berhenti karena take(3) sudah terpenuhi — 7, 8, 9, 10 tidak diproses)
println(hasil) // [4, 16, 36]
Konversi Sequence ke Collection #
Setelah operasi pipeline selesai, Sequence perlu dikonversi ke Collection untuk digunakan lebih lanjut.
val seq = (1..10).asSequence().filter { it % 2 == 0 }
// Konversi ke berbagai Collection
val list: List<Int> = seq.toList()
val set: Set<Int> = seq.toSet()
val mutableList: MutableList<Int> = seq.toMutableList()
// Ke Map
val angka = (1..5).asSequence()
val segiEmpat = angka.associateWith { it * it }
// {1=1, 2=4, 3=9, 4=16, 5=25}
// joinToString — langsung ke String tanpa List perantara
val hasil = (1..5).asSequence()
.map { it * it }
.joinToString(", ") // "1, 4, 9, 16, 25"
Ringkasan #
- Lazy evaluation adalah kunci perbedaan Sequence dari Collection — operasi intermediate (
filter,map, dll) tidak dieksekusi sampai ada terminal operation (toList(),first(),forEach(), dll).- Tidak ada list perantara — Sequence memproses elemen satu per satu melalui seluruh pipeline, sehingga menghemat memori secara signifikan untuk data besar.
- Berhenti lebih awal — operasi seperti
take(n),first { }, danfind { }berhenti memproses begitu kebutuhan terpenuhi. Ini adalah keunggulan terbesar Sequence untuk pipeline dengan filter ketat.generateSequenceuntuk Sequence yang nilainya digenerate secara dinamis, termasuk Sequence tak terbatas. Generator mengembalikannulluntuk menghentikan Sequence.sequence { yield() }builder untuk Sequence kustom dengan logika kompleks — bisa menggunakan kondisi, loop, dan state internal.yieldAlluntuk menyisipkan seluruh collection/sequence.- Gunakan Sequence untuk data besar (> ~1.000 elemen) dengan pipeline panjang. Untuk data kecil, overhead Sequence lebih besar dari manfaatnya — tetap gunakan Collection.
onEachuntuk debugging pipeline tanpa mengubah alur — lihat nilai elemen di setiap tahap pipeline.File.useLines { }adalah pola idiomatik untuk membaca file besar baris per baris dengan memori konstan — jauh lebih efisien darireadLines()yang memuat semua baris ke List.- Sequence tidak bisa digunakan ulang — setelah terminal operation dipanggil, Sequence sudah habis. Buat Sequence baru jika butuh iterasi kedua.
- sorted(), groupBy(), dan operasi yang butuh seluruh elemen tidak mendapat manfaat lazy dari Sequence — operasi ini tetap membutuhkan semua elemen sebelum bisa dilanjutkan.