Metode ekstensi GDK mengubah koleksi menjadi saluran pemrosesan data yang disusun tahap demi tahap, yakni pipeline. Topik ini membahas lima metode intinya, yaitu findAll untuk menyaring, collect untuk mengubah, groupBy untuk mengelompokkan, inject untuk menumpuk nilai, serta sort untuk mengurutkan. Setelah membaca topik ini, sebuah daftar data mentah dapat diolah menjadi ringkasan yang siap ditampilkan hanya dengan beberapa baris closure yang berantai.
Menyaring dengan findAll#
Metode findAll mengembalikan elemen-elemen yang membuat closure-nya bernilai benar, dan list aslinya tidak berubah. Closure menerima satu parameter untuk list, sedangkan pada map closure menerima dua parameter, yaitu kunci dan nilai, dan hasilnya tetap berupa map yang hanya memuat entri terpilih. Urutan elemen pada hasil mengikuti urutan aslinya, dan bila tidak ada satu pun elemen yang cocok, hasilnya tetap berupa list atau map kosong, bukan null. Peran ini menjadikan findAll tahap penyaring yang lazim dipasang sebelum elemen terpilih diubah oleh tahap berikutnya.
def skor = [90, 75, 88, 70]
def tinggi = skor.findAll { it >= 80 }
println tinggi
println skor[90, 88]
[90, 75, 88, 70]Baris kedua keluaran menegaskan bahwa findAll menghasilkan list baru, karena skor tetap memuat keempat nilai aslinya setelah penyaringan. Pada map, bentuk dua parameter memisahkan kunci dari nilainya sehingga penyaringan dapat mendasarkan diri pada salah satunya. Keluaran [Ali:90, Citra:88] mempertahankan pasangan utuhnya, sehingga nama tetap melekat pada skornya tanpa pencocokan manual.
def nilai = [Ali: 90, Budi: 75, Citra: 88, Dewi: 70]
def memuaskan = nilai.findAll { nama, skor -> skor >= 80 }
println memuaskan[Ali:90, Citra:88]Mengubah dengan collect#
Metode collect menerapkan closure pada setiap elemen dan mengumpulkan hasilnya menjadi list baru. Bentuk ini adalah pengubah bentuk data, bukan penyaring, karena jumlah elemen keluarannya sama dengan masukannya. Pada map, closure satu parameter menerima objek Entry, sedangkan closure dua parameter menerima kunci dan nilai secara terpisah.
def skor = [90, 75, 88, 70]
def tambahan = skor.collect { it + 5 }
println tambahan
def pasangan = [Ali: 90, Budi: 75].collect { kunci, nilai -> "$kunci = $nilai" }
println pasangan[95, 80, 93, 75]
[Ali = 90, Budi = 75]Karena keluaran collect juga sebuah list, hasilnya dapat langsung diberikan kepada metode pipeline berikutnya. Pola inilah yang membuat tahapan pemrosesan tersambung tanpa variabel perantara apabila dibutuhkan. Untuk rantai yang panjang, penulisan bertahap dengan variabel perantara justru lebih mudah diperiksa, karena nilai setiap tahap dapat dicetak satu per satu ketika hasilnya menyimpang. Pilihan keduanya bukan soal benar salah, melainkan soal seberapa banyak titik pemeriksaan yang dibutuhkan.
Mengelompokkan dengan groupBy#
Metode groupBy membagi elemen ke dalam map berdasarkan nilai yang dikembalikan closure-nya, dan hasilnya adalah LinkedHashMap yang kuncinya berasal dari nilai kembalian closure tersebut. Setiap kunci memetakan ke list yang memuat seluruh elemen anggota kelompoknya, sehingga struktur hasilnya berlapis satu tingkat. Kunci baru terdaftar pada kemunculan pertamanya, sehingga urutan kunci mengikuti urutan elemen yang lebih dahulu masuk ke setiap kelompok. Karena anggota kelompok berupa list, hasil groupBy dapat langsung diproses lagi oleh metode pipeline lain pada tingkat kelompok.
def skor = [90, 75, 88, 70]
def kelompok = skor.groupBy { it >= 80 ? 'tinggi' : 'sedang' }
println kelompok
println kelompok['tinggi']
println kelompok.getClass().simpleName[tinggi:[90, 88], sedang:[75, 70]]
[90, 88]
LinkedHashMapAkses kelompok['tinggi'] langsung menghasilkan list anggota kelompok tersebut. Apabila sebuah kunci kelompok tidak memiliki anggota sama sekali, kuncinya tidak akan muncul di dalam map hasil, sehingga pembacaan kunci yang tidak terdaftar menghasilkan null. Konsekuensinya, kelompok yang mungkin kosong perlu diperiksa dengan containsKey terlebih dahulu sebelum anggotanya diolah.
Menumpuk nilai dengan inject#
Metode inject menggabungkan seluruh elemen menjadi satu nilai dengan membawa akumulasi dari elemen ke elemen. Bentuk dengan argumen pertama menetapkan nilai awal akumulasi, sedangkan bentuk tanpa argumen memakai elemen pertama sebagai nilai awalnya dan mulai menggabungkan dari elemen kedua. Empat metode lain pada topik ini mengembalikan koleksi, sedangkan inject justru menutup rantai dengan satu nilai akhir. Nilai awal yang ditetapkan juga menentukan tipe akumulasi, misalnya 0 untuk penjumlahan bilangan bulat atau [] untuk pembangunan list baru.
def skor = [90, 75, 88, 70]
def total = skor.inject(0) { akumulasi, n -> akumulasi + n }
println total
def gabung = ['Groovy', 'mudah', 'dibaca'].inject { a, b -> "$a $b" }
println gabung323
Groovy mudah dibacaNilai 323 diperoleh dari 0 + 90 + 75 + 88 + 70. Pola akumulasi yang sama berlaku untuk penggabungan string, penghitungan rata-rata, maupun pembangunan map baru. Satu catatan perilaku penting: bentuk tanpa nilai awal melempar NoSuchElementException apabila koleksinya kosong, karena tidak ada elemen yang dapat menjadi akumulasi pertama.
Mengurutkan dengan sort#
Metode sort tanpa argumen mengurutkan elemen mengikuti urutan alaminya. Dengan closure satu parameter, urutan ditentukan dari nilai kembalian closure, sedangkan closure dua parameter membandingkan dua elemen dan lazim memakai operator pesawat luar angkasa (<=>), yaitu operator perbandingan yang mengembalikan nilai negatif, nol, atau positif. Pada closure dua parameter, hasil negatif menempatkan elemen pertama di depan, hasil nol menyatakan keduanya setara, dan hasil positif menempatkan elemen kedua di depan. Membalik kedua operand, misalnya menulis b <=> a alih-alih a <=> b, otomatis membalik urutan hasilnya.
def kata = ['kiwi', 'jeruk', 'apel']
kata.sort { a, b -> a.length() <=> b.length() }
println kata[kiwi, apel, jeruk]Perbedaan yang menentukan adalah sort memutasi list yang memanggilnya, sedangkan toSorted mengurutkan salinan dan membiarkan list aslinya utuh. Mutasi ini mudah luput karena nama metodenya tidak menunjukkan efek samping, sehingga list yang masih dipakai bagian kode lain dapat berubah urutan tanpa jejak. Pilih toSorted apabila urutan asli masih diperlukan di tahap lain, misalnya ketika list yang sama diurutkan dengan dua kriteria berbeda untuk dua ringkasan. Bentuk sort tetap tepat ketika list itu hanya dipakai satu tahap dan tidak dibagikan.
// sort memutasi list asli, toSorted menyalin dahulu
def asli = [90, 75, 88, 70]
def salinan = asli.toSorted()
println "asli : $asli"
println "salinan : $salinan"
def urut = [90, 75, 88, 70]
urut.sort()
println "urut : $urut"asli : [90, 75, 88, 70]
salinan : [70, 75, 88, 90]
urut : [70, 75, 88, 90]Menyusun tahapan menjadi pipeline#
Kelima metode di atas menjadi kuat ketika dirangkai, karena setiap tahap mengembalikan koleksi yang siap dipakai tahap berikutnya. Aliran datanya bergerak dari kiri ke kanan, mengikuti urutan penulisan tahapan pada rantai pemanggilannya. Berkas berikut mengolah daftar hasil ujian menjadi empat ringkasan, yaitu daftar yang lulus, anggota kelas tertentu, rata-rata nilai, dan urutan peringkat.
// Pipeline nilai ujian: menyaring, mengubah, mengelompokkan, mengurutkan
def ujian = [
[nama: 'Ali', kelas: 'A', nilai: 90],
[nama: 'Budi', kelas: 'B', nilai: 75],
[nama: 'Citra', kelas: 'A', nilai: 88],
[nama: 'Dewi', kelas: 'B', nilai: 70],
]
def lulus = ujian.findAll { it.nilai >= 80 }
println "Lulus : ${lulus.collect { it.nama }}"
def perKelas = ujian.groupBy { it.kelas }
println "Kelas A : ${perKelas['A'].collect { it.nama }}"
def total = ujian.collect { it.nilai }.inject(0) { akumulasi, n -> akumulasi + n }
println "Rata-rata : ${total / ujian.size()}"
def peringkat = ujian.sort { a, b -> b.nilai <=> a.nilai }.collect { it.nama }
println "Peringkat : $peringkat"Lulus : [Ali, Citra]
Kelas A : [Ali, Citra]
Rata-rata : 80.75
Peringkat : [Ali, Citra, Budi, Dewi]Tahapan pada berkas tersebut memperlihatkan pola kerja yang dapat dibaca sebagai satu kalimat, misalnya sort { ... }.collect { it.nama } yang berarti mengurutkan dahulu, lalu mengambil namanya. Nilai rata-rata 80.75 muncul sebagai bilangan desimal karena operator pembagian Groovy menghasilkan BigDecimal untuk operand bilangan bulat. Perlu dicatat bahwa baris peringkat memanggil sort yang memutasi ujian, sehingga urutan daftar itu berubah menjadi terurut menurun setelah baris tersebut dijalankan. Apabila urutan aslinya masih dibutuhkan, toSorted pada tahap peringkat adalah pilihan yang lebih aman.
Kesalahan umum#
Kesalahan pertama adalah menganggap hasil collect pada map sebagai map. Metode collect selalu menghasilkan list, dan closure satu parameter menerima objek Entry yang kuncinya diakses melalui properti key. Apabila bentuk map tetap diinginkan, findAll adalah metode yang tepat karena hasil penyaringan pada map memang tetap berupa map.
def nilai = [Ali: 90, Budi: 75]
def entri = nilai.collect { it }
println entri
println entri[0].getClass().simpleName
println entri*.key[Ali=90, Budi=75]
Entry
[Ali, Budi]Kesalahan kedua adalah memanggil inject tanpa nilai awal pada koleksi yang kosong. Karena tidak ada elemen yang dapat menjadi akumulasi pertama, Groovy melempar NoSuchElementException. Bentuk inject(0) yang selalu menyediakan nilai awal terhindar dari kegagalan ini.
try {
[].inject { a, b -> a + b }
} catch (NoSuchElementException e) {
println "Gagal: ${e.getClass().simpleName}"
}Gagal: NoSuchElementExceptionRingkasan#
findAllmenyaring tanpa mengubah koleksi asli, dan pada map hasilnya tetap berupa map.collectmengubah setiap elemen menjadi list baru, sedangkangroupBymenghasilkanLinkedHashMapyang nilainya berupa list.injectmenumpuk elemen menjadi satu nilai, dan bentuk tanpa nilai awal gagal pada koleksi kosong.sortmemutasi list yang memanggilnya, sedangkantoSortedmenghasilkan salinan terurut.
Referensi#
- Documentation (panduan bahasa lengkap) — Apache Groovy — bab Working with collections memuat penyaringan, pengelompokan, pengurutan, dan operator spread pada koleksi.
- Groovy JDK (GDK) API — Apache Groovy
— rujukan lengkap metode ekstensi GDK pada kelas
Collection,List, danMap.