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.

kotlin Perbandingan visual chain
// 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 elemen

Sequence 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:

SkenarioAlasan
Koleksi besar (>1000 elemen) + banyak operasi chainHemat alokasi intermediate list
Early exit (first(), take(n))Sequence berhenti saat cukup
Infinite streamSequence bisa lazy infinite
Operasi chain mendalam (5+ operasi)Akumulasi intermediate list mahal

Sequence kalah pada skenario:

SkenarioAlasan
Koleksi kecil (<100 elemen)Overhead Iterator tidak sepadan
Single operationList lebih cepat tanpa overhead
Hasil akhir butuh ListSequence.toList() menambah overhead
Debug step-by-stepIntermediate 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):

kotlin
// 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:

text
=== 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 chain

Mari kita bedah hasil pengukuran di atas:

  1. 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.
  2. 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.
  3. Single operation: List menang karena tidak ada intermediate list yang dibuat. Sequence.map.toList membayar overhead Iterator tanpa keuntungan apa pun.
  4. Early exit: list.asSequence().map { ... }.first() hanya memproses elemen pertama. List harus memproses semua elemen dulu sebelum first() 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.

kotlin Early exit dengan Sequence
// 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 filter

Pada 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.

kotlin Infinite sequence
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 Sequence Kotlin 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.