Collection pada Kotlin diproses secara eager, yaitu setiap operasi selesai sepenuhnya sebelum operasi berikutnya dimulai. Sequence menawarkan cara lain, yaitu evaluasi lazy yang memproses elemen satu per satu sepanjang rantai operator. Topik ini membandingkan keduanya dengan program yang mencetak jejak pemrosesannya, menghitung berapa kali setiap lambda dipanggil, lalu menutup dengan kondisi ketika Sequence memberi keuntungan. Seluruh keluaran berasal dari eksekusi nyata dengan compiler Kotlin.
Urutan evaluasi pada collection eager#
Pada pemrosesan eager, fungsi map dan filter pada collection dieksekusi satu per satu sampai tuntas. Pemanggilan map menghabiskan seluruh elemen dan membentuk List perantara, kemudian filter membaca daftar perantara itu dan membentuk List hasilnya. Program berikut mencetak jejak dari setiap pemanggilan lambda, sehingga urutan evaluasinya terlihat secara langsung.
fun main() {
val angka = listOf(1, 2, 3, 4, 5)
println("eager:")
val hasilEager = angka
.map { println("map $it"); it * 10 }
.filter { println("filter $it"); it % 20 == 0 }
println(hasilEager)
println("sequence:")
val hasilLazy = angka.asSequence()
.map { println("map $it"); it * 10 }
.filter { println("filter $it"); it % 20 == 0 }
.toList()
println(hasilLazy)
}# compiler Kotlin dan pustaka standar yang dipakai jalur ini
export PATH=/home/kal/my-project/tools/kotlinc/bin:/home/kal/my-project/tools/jdk-merged/bin:$PATH
export STDLIB=/home/kal/my-project/tools/kotlinc/lib/kotlin-stdlib.jar
# kompilasi main.kt menjadi out.jar, lalu jalankan programnya
kotlinc main.kt -d out.jar && java -cp "out.jar:$STDLIB" MainKteager:
map 1
map 2
map 3
map 4
map 5
filter 10
filter 20
filter 30
filter 40
filter 50
[20, 40]
sequence:
map 1
filter 10
map 2
filter 20
map 3
filter 30
map 4
filter 40
map 5
filter 50
[20, 40]Bagian eager: pada keluaran memperlihatkan lima baris map lebih dulu, lalu lima baris filter, karena map menuntaskan seluruh elemen sebelum filter mulai bekerja. Nilai 1 sampai 5 dilipatgandakan sepuluh kali menjadi 10 sampai 50 pada daftar perantara, dan baru kemudian disaring dengan syarat it % 20 == 0 sehingga tersisa [20, 40]. Jejak seperti ini bisa dibaca sebagai kronologis kerja program, dan urutannya tetap sama untuk rantai operator apa pun pada collection.
Evaluasi per elemen pada Sequence#
Bagian sequence: pada keluaran yang sama menunjukkan pola berbeda. Pemanggilan asSequence() membungkus collection menjadi Sequence, dan operator perantara seperti map serta filter hanya menyiapkan rantai pemrosesan tanpa menyentuh elemennya. Pemrosesan baru berjalan ketika operator terminal dipanggil, dan pada program ini terminalnya adalah toList().
Keluaran pada bagian tersebut saling berselang-seling, yaitu map 1 diikuti filter 10, lalu map 2 diikuti filter 20, dan seterusnya sampai elemen terakhir. Pola ini membuktikan bahwa satu elemen melewati seluruh rantai operator lebih dulu sebelum elemen berikutnya dimulai. Tidak ada List perantara yang dibentuk, karena hasil setiap operator dikirim langsung ke operator berikutnya sebagai aliran elemen. Hasil akhirnya sama dengan versi eager, yaitu [20, 40], sedangkan jalannya pekerjaan berbeda.
Jumlah operasi dan urutan operator#
Selisih perilaku keduanya menjadi terukur ketika jumlah pemanggilan lambda dihitung. Program berikut memakai variabel penghitung di dalam lambda, lalu membandingkan pencarian hasil pertama pada seratus elemen, termasuk pengaruh urutan operator pada Sequence.
fun main() {
val angka = (1..100).toList()
var hitungMap = 0
val hasilEager = angka.map { hitungMap++; it * 2 }.first { it > 10 }
println("eager: map $hitungMap kali, hasil $hasilEager")
hitungMap = 0
val hasilLazy = angka.asSequence().map { hitungMap++; it * 2 }.first { it > 10 }
println("sequence: map $hitungMap kali, hasil $hasilLazy")
var hitungFilter = 0
hitungMap = 0
val hasilEfisien = angka.asSequence()
.filter { hitungFilter++; it % 4 == 0 }
.map { hitungMap++; it * 2 }
.toList()
println("filter dulu: filter $hitungFilter kali, map $hitungMap kali")
hitungFilter = 0
hitungMap = 0
val hasilBalik = angka.asSequence()
.map { hitungMap++; it * 2 }
.filter { hitungFilter++; it % 8 == 0 }
.toList()
println("map dulu: filter $hitungFilter kali, map $hitungMap kali")
}# kompilasi operasi.kt menjadi operasi.jar, lalu jalankan programnya
kotlinc operasi.kt -d operasi.jar && java -cp "operasi.jar:$STDLIB" OperasiKteager: map 100 kali, hasil 12
sequence: map 6 kali, hasil 12
filter dulu: filter 100 kali, map 25 kali
map dulu: filter 100 kali, map 100 kaliDua baris pertama membandingkan pencarian hasil pertama. Pemrosesan eager memanggil map seratus kali karena seluruh hasil harus terbentuk lebih dulu, sedangkan sequence hanya memanggilnya enam kali dan berhenti begitu syarat it > 10 terpenuhi. Dua baris berikutnya memperlihatkan pengaruh urutan operator, yaitu filter yang ditempatkan lebih dulu membuat map hanya dipanggil 25 kali untuk seratus elemen, sedangkan urutan terbalik memanggil keduanya seratus kali. Urutan operator pada Sequence bukan sekadar gaya penulisan, melainkan menentukan jumlah pekerjaan yang benar-benar dilakukan.
Kapan Sequence menguntungkan#
Sequence memberi keuntungan pada tiga kondisi, yaitu collection berukuran besar, rantai operator yang panjang, dan operator terminal yang berhenti lebih awal seperti first. Ketiganya berkaitan dengan hal yang sama, yaitu penghematan karena elemen yang tidak diperlukan tidak pernah diproses dan daftar perantara tidak terbentuk.
Keuntungan itu ada biayanya. Setiap operator perantara pada Sequence menambah satu objek iterator dan satu lapisan pemanggilan, sehingga untuk collection kecil dengan satu atau dua operator, biaya tambahan itu biasanya tidak dibayar kembali oleh penghematannya. Collection biasa tetap menjadi pilihan yang wajar pada program harian, sedangkan asSequence() ditambahkan ketika ukuran data atau panjang rantainya membenarkan.
Ringkasan#
- Collection diproses eager, yaitu setiap operator menuntaskan seluruh elemen dan membentuk list perantara sebelum operator berikutnya berjalan.
Sequencediproses lazy per elemen, yaitu satu elemen melewati seluruh rantai operator sebelum elemen berikutnya, dan hanya berjalan setelah operator terminal dipanggil.- Urutan operator menentukan jumlah pekerjaan, sehingga
filteryang ditempatkan lebih dulu mengurangi pemanggilan operator setelahnya. Sequencemenguntungkan untuk data besar, rantai panjang, atau operator terminal yang berhenti awal, sedangkan collection biasa lebih efisien untuk data kecil.
Referensi#
- Sequences — Kotlin Documentation — Evaluasi lazy, perbedaan operator perantara dan terminal, serta perbandingannya dengan collection.
- Collections overview — Kotlin Documentation
— Peta keluarga collection Kotlin sebagai pembanding konsep
Sequence.