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:
- Memproses seluruh elemen
listmelaluimap, menghasilkan list pertama. - Memproses seluruh elemen list pertama melalui
filter, menghasilkan list kedua. - 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:
- Meminta elemen pertama dari
take(5). takememinta elemen pertama darifilter.filtermeminta elemen darimapsampai menemukan satu yang lolos.mapmengambil elemen dari source.- Setelah
takemendapat 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 #
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:
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 internalMari kita bedah hasil pengukuran di atas:
- Eager
mapdanfiltermemproses 1.000.000 elemen: walaupun kita hanya mengambil 5 elemen hasil,mapdanfilterharus memproses seluruh list karena operasi eager mengevaluasi penuh sebelum lanjut ke tahap berikutnya. - Lazy
mapdanfiltermemproses 1.005 elemen: Sequence memproses elemen satu per satu. Setelahtake(5)mendapat 5 elemen yang lolos filter, pipeline berhenti. Sisa 998.995 elemen tidak pernah disentuh. first { it > 100 }pada eager memproses 1.000.000 elemen:mapharus mengubah seluruh list sebelumfirstbisa mencari. Padahal121 = 11^2sudah muncul pada elemen ke-11.first { it > 100 }pada lazy memproses 11 elemen: Sequence langsung berhenti setelah menemukan121. Inilah keuntungan utama lazy untuk operasi short-circuit sepertifirst,find,any,take.- 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.
- Sequence tidak mendukung
seq[0]atauseq.size: Sequence tidak menyimpan elemen, sehingga tidak ada akses indeks. Untuk akses acak, materialize ke List dengantoList()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 begitunelemen terkumpul, eager memproses semua elemen dulu.- Short-circuit dengan
first/find/any: lazy berhenti begitu kondisi terpenuhi. - Deret tak terbatas atau generatif:
generateSequencedan buildersequencehanya mungkin dalam dunia lazy.
Kapan List Menang #
List lebih cocok ketika:
- Akses acak berulang:
list[i]adalahO(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 #
| Aspek | Eager (List) | Lazy (Sequence) |
|---|---|---|
| Evaluasi | Segera | Saat terminal dipanggil |
| Alokasi list 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 | List antara | Buffer internal |
| Cocok untuk | Multi-pass, akses acak | Pipeline panjang, deret tak terbatas |
Referensi Lanjutan #
- Sequences
— perbandingan lazy
Sequencevs eagerList - Sequence API reference
— API reference
kotlin.sequences
Setelah memahami semua jenis collection dan operasinya, kita akan merangkum panduan memilih collection yang tepat untuk setiap skenario pada chapter penutup section ini.