Pipeline operasi collection bisa dievaluasi dengan dua strategi: eager dan lazy. Strategi eager mengevaluasi setiap tahap secara penuh dan membuat list antara; strategi lazy mengevaluasi elemen demi elemen sepanjang pipeline. Pada chapter ini kita akan membandingkan kedua pendekatan secara langsung, melihat kapan Sequence unggul dan kapan List lebih cocok, lalu menutup dengan tabel ringkas perbedaan keduanya.

Cara Kerja Eager (List) #

Pada pipeline List, setiap operasi intermediate (map, filter, take, dll.) segera mengevaluasi seluruh collection dan membuat list baru. Pipeline seperti list.map { ... }.filter { ... }.take(5) akan:

  1. Memproses seluruh elemen list melalui map, menghasilkan list pertama.
  2. Memproses seluruh elemen list pertama melalui filter, menghasilkan list kedua.
  3. Mengambil 5 elemen pertama dari list kedua.

Total: tiga iterasi penuh, tiga list dialokasikan. Bila koleksi berukuran satu juta elemen dan kita hanya butuh 5 elemen hasil, eager tetap memproses satu juta elemen di setiap tahap.

Cara Kerja Lazy (Sequence) #

Pada pipeline Sequence, operasi intermediate tidak mengevaluasi apa pun. Evaluasi baru terjadi saat operasi terminal dipanggil. Saat itu, setiap elemen mengalir melalui seluruh pipeline sebelum elemen berikutnya diproses.

Pipeline seq.map { ... }.filter { ... }.take(5).toList() akan:

  1. Meminta elemen pertama dari take(5).
  2. take meminta elemen pertama dari filter.
  3. filter meminta elemen dari map sampai menemukan satu yang lolos.
  4. map mengambil elemen dari source.
  5. Setelah take mendapat 5 elemen, seluruh pipeline berhenti — sisa koleksi tidak diproses.

Perbandingan Jumlah Elemen yang Diproses #

Mari kita ukur berapa banyak elemen yang diproses oleh masing-masing strategi pada list satu juta elemen dengan take(5):

Contoh Lengkap #

kotlin
fun main() {
    val besar = (1..1_000_000).toList()

    // === Eager: tiap tahap membuat list antara ===
    // Hitung jumlah elemen yang diproses pada setiap tahap
    var hitungMapEager = 0
    var hitungFilterEager = 0
    val hasilEager = besar
        .map { hitungMapEager++; it * it }
        .filter { hitungFilterEager++; it > 1_000_000 }
        .take(5)
    println("Eager: hasil=${hasilEager.size}")
    println("  elemen diproses map: $hitungMapEager")
    println("  elemen diproses filter: $hitungFilterEager")

    // === Lazy: pipeline dievaluasi elemen demi elemen ===
    var hitungMapLazy = 0
    var hitungFilterLazy = 0
    val hasilLazy = besar
        .asSequence()
        .map { hitungMapLazy++; it * it }
        .filter { hitungFilterLazy++; it > 1_000_000 }
        .take(5)
        .toList()
    println("Lazy: hasil=${hasilLazy.size}")
    println("  elemen diproses map: $hitungMapLazy")
    println("  elemen diproses filter: $hitungFilterLazy")

    // === Short-circuit: find pada Sequence berhenti lebih awal ===
    var diprosesEager = 0
    val hasilEagerFind = besar.map { diprosesEager++; it * it }.first { it > 100 }
    println("Eager find first: hasil=$hasilEagerFind, elemen diproses=$diprosesEager")

    var diprosesLazy = 0
    val hasilLazyFind = besar.asSequence()
        .map { diprosesLazy++; it * it }
        .first { it > 100 }
    println("Lazy  find first: hasil=$hasilLazyFind, elemen diproses=$diprosesLazy")

    // === Kapan eager menang: akses multiple pass ===
    val list = (1..100).toList()
    // List bisa dipakai berkali-kali
    val jumlah = list.sum()
    val max = list.maxOrNull()
    val avg = list.average()
    println("List multi-pass: sum=$jumlah, max=$max, avg=$avg")

    // === Kapan lazy menang: chain banyak operasi + take ===
    val hasilLazyTake = (2..1_000_000).asSequence()
        .map { it * it }
        .filter { it % 7 == 0 }
        .take(5)
        .toList()
    println("Lazy take 5 kelipatan 7: $hasilLazyTake")

    // === Sequence tidak mendukung akses indeks ===
    val seq = listOf(10, 20, 30).asSequence()
    // seq[0]  // compile error: Sequence tidak punya operator get
    // seq.size  // tidak ada properti size
    val viaList = seq.toList()
    println("Sequence di-materialize ke List untuk akses indeks: indeks 0 = ${viaList[0]}")

    // === Tabel perbandingan karakteristik ===
    println("--- Perbandingan ---")
    println("Aspek            | Eager (List)        | Lazy (Sequence)")
    println("Evaluasi         | Segera              | Saat terminal")
    println("Alokasi antara   | Setiap tahap        | Tanpa (single pass)")
    println("Iterasi ulang    | Ya                  | Tidak (one-shot)")
    println("Akses indeks     | O(1)                | Tidak didukung")
    println("Short-circuit    | Tidak optimal       | Optimal (berhenti awal)")
    println("Sorted/Distinct  | Membuat list antara | Buffer internal")
}

Output yang Dihasilkan:

text
Eager: hasil=5
  elemen diproses map: 1000000
  elemen diproses filter: 1000000
Lazy: hasil=5
  elemen diproses map: 1005
  elemen diproses filter: 1005
Eager find first: hasil=121, elemen diproses=1000000
Lazy  find first: hasil=121, elemen diproses=11
List multi-pass: sum=5050, max=100, avg=50.5
Lazy take 5 kelipatan 7: [49, 196, 441, 784, 1225]
Sequence di-materialize ke List untuk akses indeks: indeks 0 = 10
--- Perbandingan ---
Aspek            | Eager (List)        | Lazy (Sequence)
Evaluasi         | Segera              | Saat terminal
Alokasi antara   | Setiap tahap        | Tanpa (single pass)
Iterasi ulang    | Ya                  | Tidak (one-shot)
Akses indeks     | O(1)                | Tidak didukung
Short-circuit    | Tidak optimal       | Optimal (berhenti awal)
Sorted/Distinct  | Membuat list antara | Buffer internal

Mari kita bedah hasil pengukuran di atas:

  1. Eager map dan filter memproses 1.000.000 elemen: walaupun kita hanya mengambil 5 elemen hasil, map dan filter harus memproses seluruh list karena operasi eager mengevaluasi penuh sebelum lanjut ke tahap berikutnya.
  2. Lazy map dan filter memproses 1.005 elemen: Sequence memproses elemen satu per satu. Setelah take(5) mendapat 5 elemen yang lolos filter, pipeline berhenti. Sisa 998.995 elemen tidak pernah disentuh.
  3. first { it > 100 } pada eager memproses 1.000.000 elemen: map harus mengubah seluruh list sebelum first bisa mencari. Padahal 121 = 11^2 sudah muncul pada elemen ke-11.
  4. first { it > 100 } pada lazy memproses 11 elemen: Sequence langsung berhenti setelah menemukan 121. Inilah keuntungan utama lazy untuk operasi short-circuit seperti first, find, any, take.
  5. List multi-pass: ketika kita perlu mengakses koleksi beberapa kali (sum, max, average), List adalah pilihan tepat. Sequence bersifat one-shot — diiterasi sekali lalu habis.
  6. Sequence tidak mendukung seq[0] atau seq.size: Sequence tidak menyimpan elemen, sehingga tidak ada akses indeks. Untuk akses acak, materialize ke List dengan toList() lebih dulu.

Kapan Sequence Menang #

Sequence unggul pada skenario berikut:

  • Collection besar + pipeline panjang: setiap tahap eager mengalokasikan list antara yang besar. Lazy menghindari alokasi tersebut.
  • take(n) di akhir pipeline: lazy berhenti begitu n elemen terkumpul, eager memproses semua elemen dulu.
  • Short-circuit dengan first/find/any: lazy berhenti begitu kondisi terpenuhi.
  • Deret tak terbatas atau generatif: generateSequence dan builder sequence hanya mungkin dalam dunia lazy.

Kapan List Menang #

List lebih cocok ketika:

  • Akses acak berulang: list[i] adalah O(1). Sequence harus dimaterialize lebih dulu.
  • Multi-pass: list yang sama bisa diiterasi berkali-kali.
  • Collection kecil + sedikit operasi: overhead dispatch per elemen pada Sequence lebih besar daripada penghematan alokasi list.
  • Operasi sorted/distinct: kedua operasi pada Sequence memerlukan buffer internal yang menyimpan seluruh elemen, sehingga menghapus keuntungan lazy.

Tabel Ringkas #

AspekEager (List)Lazy (Sequence)
EvaluasiSegeraSaat terminal dipanggil
Alokasi list antaraSetiap tahapTanpa (single pass)
Iterasi ulangYaTidak (one-shot)
Akses indeksO(1)Tidak didukung
Short-circuitTidak optimalOptimal (berhenti awal)
sorted/distinctList antaraBuffer internal
Cocok untukMulti-pass, akses acakPipeline panjang, deret tak terbatas

Referensi Lanjutan #

Setelah memahami semua jenis collection dan operasinya, kita akan merangkum panduan memilih collection yang tepat untuk setiap skenario pada chapter penutup section ini.