Koleksi List di Kotlin bersifat eager: setiap operasi seperti map dan filter membuat List baru. Untuk chain seperti list.map.filter.map.sum, ada tiga intermediate list yang dibuat — masing-masing berisi elemen hasil. Sequence adalah alternatif lazy yang tidak membuat intermediate list, melainkan memproses elemen satu per satu melalui seluruh pipeline. Pada bab ini kita akan membandingkan keduanya dengan step counter, mendemokan kapan Sequence unggul, dan menjelaskan kapan List tetap menjadi pilihan lebih baik.
List vs Sequence #
List<T> di Kotlin adalah koleksi eager yang menyimpan seluruh elemen di memori. Operasi map, filter, flatMap membuat List baru yang berisi hasil. Sequence<T> adalah stream lazy yang hanya menyimpan referensi ke operasi-operasi yang harus dilakukan. Elemen diproses satu per satu saat terminal operation (sum, toList, count) dipanggil.
// List: 3 intermediate lists
list.map { it * 2 } // List baru size 1000
.filter { it > 500 } // List baru size 500
.map { it + 1 } // List baru size 500
.sum() // Akhir: 4 list objects di memori
// Sequence: 0 intermediate lists
list.asSequence()
.map { it * 2 } // Hanya lambda
.filter { it > 500 } // Hanya lambda
.map { it + 1 } // Hanya lambda
.sum() // Trigger: proses elemen per elemenSequence mengganti 3 list intermediate dengan 3 Iterator wrapper. Iterator wrapper jauh lebih ringan (beberapa byte) daripada list (array backing + elemen).
Kapan Sequence Menang #
Sequence unggul pada skenario berikut:
| Skenario | Alasan |
|---|---|
| Koleksi besar (>1000 elemen) + banyak operasi chain | Hemat alokasi intermediate list |
Early exit (first(), take(n)) | Sequence berhenti saat cukup |
| Infinite stream | Sequence bisa lazy infinite |
| Operasi chain mendalam (5+ operasi) | Akumulasi intermediate list mahal |
Sequence kalah pada skenario:
| Skenario | Alasan |
|---|---|
| Koleksi kecil (<100 elemen) | Overhead Iterator tidak sepadan |
| Single operation | List lebih cepat tanpa overhead |
| Hasil akhir butuh List | Sequence.toList() menambah overhead |
| Debug step-by-step | Intermediate List bisa di-inspect |
Demo Step-Counting #
Cuplikan berikut membandingkan chain map.filter.map.sum pada 1000 elemen, dengan dan tanpa Sequence. Counter melacak steps (lambda invocations) dan allocations (intermediate list):
// Demo sequence vs List: hitung step dan allocation pada chain operations.
// Sequence lazy evaluation menghemat alokasi intermediate list.
class OpCounter {
var steps: Long = 0
var allocations: Long = 0
fun reset() { steps = 0; allocations = 0 }
fun step(n: Long = 1) { steps += n }
fun alloc(n: Long = 1) { allocations += n }
fun report(label: String): String =
" %-30s steps=%-12d allocations=%-6d".format(label, steps, allocations)
}
fun main() {
println("=== Sequence vs List Performance ===")
println()
println("Operasi: list.map.filter.map.sum")
println("Input: 1..1000")
println()
val counter = OpCounter()
val input = (1..1000).toList()
// === 1. List chain: setiap step membuat list baru ===
println("=== 1. List chain (eager) ===")
counter.reset()
val result1 = input
.map { counter.step(); counter.alloc(); it * 2 } // 1000 ops, 1 list baru
.filter { counter.step(); counter.alloc(); it > 500 } // 1000 ops, 1 list baru
.map { counter.step(); counter.alloc(); it + 1 } // 1000 ops, 1 list baru
.sum()
println(counter.report("List chain (3 intermediate lists)"))
println(" Hasil: $result1")
println(" Alokasi: 3 intermediate List objects")
println()
// === 2. Sequence chain: lazy evaluation ===
println("=== 2. Sequence chain (lazy) ===")
counter.reset()
val result2 = input.asSequence()
.map { counter.step(); it * 2 }
.filter { counter.step(); it > 500 }
.map { counter.step(); it + 1 }
.sum()
println(counter.report("Sequence chain (no intermediate list)"))
println(" Hasil: $result2")
println(" Alokasi: 0 intermediate List (hanya Iterator wrapper)")
println()
// === 3. Walk-through elemen per elemen ===
println("=== 3. Walk-through Sequence (elemen per elemen) ===")
println(" List: 1000 → map (1000) → 1000 list → filter (1000) → 250 list → map (250) → 250 list")
println(" Sequence: 1000 elemen, masing-masing melalui 3 stage, total 3000 ops")
println(" tanpa membuat list intermediate")
println()
// === 4. Single pass: List lebih cepat ===
println("=== 4. Single operation (List unggul) ===")
counter.reset()
val result3 = input.map { counter.step(); it * 2 }
println(counter.report("List.map (single op)"))
println(" List overhead rendah, Sequence tidak ada keuntungan")
println()
counter.reset()
val result4 = input.asSequence().map { counter.step(); it * 2 }.toList()
println(counter.report("Sequence.map.toList"))
println(" Sequence membayar overhead Iterator tanpa untung")
println()
// === 5. When to choose ===
println("=== 5. Kapan Memilih ===")
val cases = listOf(
Triple("Koleksi besar + banyak operasi chain", "Sequence", "Hemat alokasi intermediate"),
Triple("Koleksi kecil + 1-2 operasi", "List", "Overhead Iterator tidak sepadan"),
Triple("Butuh hasil List di akhir", "List", "Sequence.toList menambah overhead"),
Triple("Infinite stream / lazy infinite", "Sequence", "List tidak mungkin infinite"),
Triple("Early exit (first / take)", "Sequence", "Tidak proses sisanya"),
Triple("Debug step-by-step", "List", "Intermediate list bisa di-inspect")
)
cases.forEach { (skenario, pilihan, alasan) ->
println(" Skenario: $skenario")
println(" Pilih: $pilihan — $alasan")
println()
}
// === 6. Tabel ringkasan ===
println("=== 6. Tabel Ringkasan ===")
val tabel = listOf(
"List.map" to "Membuat list baru seketika",
"Sequence.map" to "Wrapper Iterator, eksekusi saat terminal",
"List.filter" to "Membuat list baru seketika",
"Sequence.filter" to "Skip elemen yang false saat iterasi",
"List.sum" to "Iterasi list, akumulasi",
"Sequence.sum" to "Terminal — trigger seluruh chain"
)
tabel.forEach { (op, keterangan) ->
println(" %-25s $keterangan".format(op))
}
}Output yang Dihasilkan:
=== Sequence vs List Performance ===
Operasi: list.map.filter.map.sum
Input: 1..1000
=== 1. List chain (eager) ===
List chain (3 intermediate lists) steps=2750 allocations=2750
Hasil: 939000
Alokasi: 3 intermediate List objects
=== 2. Sequence chain (lazy) ===
Sequence chain (no intermediate list) steps=2750 allocations=0
Hasil: 939000
Alokasi: 0 intermediate List (hanya Iterator wrapper)
=== 3. Walk-through Sequence (elemen per elemen) ===
List: 1000 → map (1000) → 1000 list → filter (1000) → 250 list → map (250) → 250 list
Sequence: 1000 elemen, masing-masing melalui 3 stage, total 3000 ops
tanpa membuat list intermediate
=== 4. Single operation (List unggul) ===
List.map (single op) steps=1000 allocations=0
List overhead rendah, Sequence tidak ada keuntungan
Sequence.map.toList steps=1000 allocations=0
Sequence membayar overhead Iterator tanpa untung
=== 5. Kapan Memilih ===
Skenario: Koleksi besar + banyak operasi chain
Pilih: Sequence — Hemat alokasi intermediate
Skenario: Koleksi kecil + 1-2 operasi
Pilih: List — Overhead Iterator tidak sepadan
Skenario: Butuh hasil List di akhir
Pilih: List — Sequence.toList menambah overhead
Skenario: Infinite stream / lazy infinite
Pilih: Sequence — List tidak mungkin infinite
Skenario: Early exit (first / take)
Pilih: Sequence — Tidak proses sisanya
Skenario: Debug step-by-step
Pilih: List — Intermediate list bisa di-inspect
=== 6. Tabel Ringkasan ===
List.map Membuat list baru seketika
Sequence.map Wrapper Iterator, eksekusi saat terminal
List.filter Membuat list baru seketika
Sequence.filter Skip elemen yang false saat iterasi
List.sum Iterasi list, akumulasi
Sequence.sum Terminal — trigger seluruh chainMari kita bedah hasil pengukuran di atas:
- List chain (eager): total 2750 step (1000 map + 1000 filter + 750 map) dan 2750 alokasi (counter ditambah setiap operasi). Pada kenyataannya, alokasi intermediate list adalah 3 objek List (1 per stage), tetapi counter demo menghitung per elemen untuk menyederhanakan.
- Sequence chain (lazy): total step sama (2750), tetapi alokasi intermediate list adalah 0. Sequence hanya membuat Iterator wrapper untuk setiap stage — overhead kecil konstan, tidak bergantung pada ukuran input.
- Single operation: List menang karena tidak ada intermediate list yang dibuat.
Sequence.map.toListmembayar overhead Iterator tanpa keuntungan apa pun. - Early exit:
list.asSequence().map { ... }.first()hanya memproses elemen pertama. List harus memproses semua elemen dulu sebelumfirst()bisa dipanggil.
Early Exit Pattern #
Salah satu keuntungan terbesar Sequence adalah early exit. Operasi first(), take(n), atau any() berhenti segera setelah kondisi terpenuhi, tanpa memproses sisa koleksi.
// List: proses 1_000_000 elemen semua dulu
val big = (1..1_000_000).toList()
val first1000 = big.map { it * 2 }.filter { it > 100 }.take(5).toList()
// Bikin list 1M, lalu filter 1M, lalu take 5
// Sequence: hanya proses ~7 elemen pertama
val first1000seq = big.asSequence()
.map { it * 2 }
.filter { it > 100 }
.take(5)
.toList()
// Sequence proses elemen per elemen, berhenti setelah 5 lolos filterPada contoh di atas, List memproses 1 juta elemen hanya untuk mengambil 5. Sequence hanya memproses ~7 elemen (karena filter menolak ~2 elemen sebelum menemukan 5 yang lolos).
Infinite Sequence #
Sequence bisa merepresentasikan stream infinite karena bersifat lazy. generateSequence membuat sequence dari function generator.
val primes: Sequence<Int> = generateSequence(2) { n ->
(n + 1).takeIf { isPrime(it) }
}
val first10Primes = primes.take(10).toList()
// [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]List tidak bisa infinite karena harus menyimpan seluruh elemen di memori. Sequence lazy sehingga elemen dibuat sesuai permintaan.
Referensi Lanjutan #
- Sequences
— panduan resmi
SequenceKotlin untuk evaluasi lazy dan performa pipeline
Selanjutnya #
Sequence mengoptimalkan operasi koleksi. Bab berikutnya membahas optimasi di level concurrency: coroutine lebih ringan daripada thread, dan pilihan dispatcher yang tepat (Default vs IO) sangat memengaruhi performance. Materi tersebut ada di chapter Coroutine Performance.