GDK (Groovy Development Kit) menambahkan lebih dari 2000 metode pada kelas-kelas JDK yang sudah dikenal, sehingga angka, string, koleksi, dan berkas membawa kemampuan tambahan yang tidak ada di Java. Angka itu tidak perlu dihafal, karena pemakaiannya berupa pekerjaan kecil yang sering, seperti mengulang aksi lima kali, membalik teks, mengiris daftar nilai, atau membaca isi berkas dalam satu baris. Topik ini menjelaskan mekanisme metode ekstensi (extension method), menelusuri kelompok metode yang paling sering dipakai pada Number, String, list, map, dan berkas, lalu menutup dengan cara menemukan metode yang tepat beserta dua kesalahan yang lazim. Setelah membacanya, pekerjaan harian tersebut dapat diselesaikan tanpa menulis kelas bantu sendiri.
Metode ekstensi pada kelas JDK#
Metode ekstensi adalah metode yang didefinisikan di luar kelasnya, tetapi dapat dipanggil seolah-olah metode milik kelas tersebut. Groovy menyimpannya sebagai metode statis di kelas bantu, antara lain DefaultGroovyMethods untuk koleksi dan Number, StringGroovyMethods untuk teks, serta NioExtensions pada modul groovy-nio untuk Path. Kelas JDK aslinya tidak pernah dimodifikasi, sehingga bytecode String tetap tanpa metode reverse, padahal 'abc'.reverse() berjalan tanpa masalah. Catatan rilis (release notes) Groovy 5.0 menyebut angkanya langsung, yaitu lebih dari 2000 metode ekstensi pada 150+ kelas JDK, dengan sekitar 350 metode baru di Groovy 5.
Jalur pencariannya berbeda untuk kode dinamis dan kode statis. Pada pemanggilan dinamis, Groovy mencari metode di kelas penerima lebih dahulu, lalu meneruskan pemanggilan yang tidak ditemukan melalui MOP (Metaobject Protocol), yaitu lapisan yang mengatur cara metode dicari saat program berjalan, dan di lapisan itulah metode ekstensi terdaftar. Pada kode statis, pencarian dilakukan saat kompilasi langsung ke metode statis kelas bantunya. Perbedaan itu dapat dibuktikan lewat refleksi terhadap kelas Integer.
// deteksi-ekstensi.groovy: membandingkan refleksi kelas JDK dan kelas bantu GDK
import java.lang.reflect.Modifier
import org.codehaus.groovy.runtime.DefaultGroovyMethods
def jdk = Integer.class.getMethods().findAll { it.name == 'times' }
def gdk = DefaultGroovyMethods.getMethods().findAll { it.name == 'times' }
println "Deklarasi times di Integer.class : ${jdk.size()}"
println "Deklarasi times di DefaultGroovyMethods : ${gdk.size()}"
println "Metode static DefaultGroovyMethods : ${DefaultGroovyMethods.getMethods().count { Modifier.isStatic(it.modifiers) }}"
def baris = []
3.times { baris << "putaran-$it" }
println "3.times tetap berjalan : ${baris.join(', ')}"Deklarasi times di Integer.class : 0
Deklarasi times di DefaultGroovyMethods : 1
Metode static DefaultGroovyMethods : 1068
3.times tetap berjalan : putaran-0, putaran-1, putaran-2Refleksi terhadap Integer.class tidak menemukan satu pun metode bernama times, karena kelas Integer milik Java memang tidak mendeklarasikannya. Metode yang sama justru terdaftar di DefaultGroovyMethods sebagai satu dari sekian ribu metode statisnya. Meski begitu, 3.times tetap berjalan normal di Groovy, karena pencarian metode tidak berhenti pada kelas penerima. Inilah sebabnya metode ekstensi terasa seperti milik kelas JDK, sedangkan kode Java murni harus menulis perulangan setaranya secara manual.
Number: times, upto, downto, dan step#
Kelompok metode ekstensi pada Number memadatkan perulangan angka menjadi satu baris. Metode times menjalankan closure, yaitu blok kode yang dikirim sebagai argumen, sebanyak n kali dengan nomor urut 0 hingga n-1 sebagai parameternya. Metode upto dan downto menaik atau menurunkan penerimanya sampai batas yang diberikan, dan batas itu ikut dihitung. Metode step lebih bebas lagi, karena jarak antarnilainya ditentukan sendiri dan boleh berupa bilangan desimal. Skrip berikut memakai keempatnya dalam satu rangkaian.
5.times { print "$it " }
println()
1.upto(3) { println "putaran ke-$it" }
3.downto(1) { print "$it " }
println()
def langkah = []
1.step(2, 0.5) { langkah << it }
println "step: $langkah"0 1 2 3 4
putaran ke-1
putaran ke-2
putaran ke-3
3 2 1
step: [1, 1.5]Keluaran tersebut memperlihatkan empat pola sekaligus. Putaran times memberi nomor urut otomatis tanpa variabel pembantu, upto mencetak tiga putaran berurutan, downto menghitung mundur tanpa kondisi keluar yang ditulis manual, sedangkan step melangkah dari 1 ke 2 dengan jarak 0.5 sehingga menghasilkan 1 dan 1.5. Pada pekerjaan nyata, pola ini cukup untuk menomori lembar dokumen dan membentuk deret berjarak tetap.
String: teks, pengisi, dan konversi angka#
Metode ekstensi pada String menyediakan pengolahan teks yang pada Java memerlukan kelas bantu tambahan. Metode reverse membalik urutan karakternya, padLeft menambahkan pengisi di sisi kiri sampai panjang tertentu, sedangkan toList memecah teks menjadi daftar karakter satu per satu. Kelompok berikutnya berkaitan dengan angka, yaitu predikat isNumber dan isInteger untuk memeriksa isi teks, lalu toInteger dan toDouble untuk mengubah teks menjadi nilai yang bisa dihitung.
println 'piket'.reverse()
println '7'.padLeft(3, '0')
println '42'.padLeft(5)
println 'ali'.toList()
println "12.5 adalah angka : ${'12.5'.isNumber()}"
println "dua adalah angka : ${'dua'.isNumber()}"
println "3.14 bilangan bulat : ${'3.14'.isInteger()}"
println "'25' + 5 : ${'25'.toInteger() + 5}"
println "'2.5' dikali 2 : ${'2.5'.toDouble() * 2}"tekip
007
42
[a, l, i]
12.5 adalah angka : true
dua adalah angka : false
3.14 bilangan bulat : false
'25' + 5 : 30
'2.5' dikali 2 : 5.0'7'.padLeft(3, '0') menghasilkan 007 karena karakter pengisinya diberikan secara eksplisit, sedangkan '42'.padLeft(5) memakai spasi karena argumen kedua tidak disediakan. Predikat isNumber menerima 12.5 karena teks itu bilangan desimal yang sah, sedangkan isInteger menolaknya, dan kedua predikat itu sama-sama menolak teks dua. Hasil konversi pada dua baris terakhir langsung ikut operasi aritmetika, sehingga pemeriksaan dan penghitungan dapat dirangkai dalam satu ekspresi.
Metode pada list dan map#
Koleksi adalah wilayah GDK yang paling padat metodenya. Pada list, first dan last mengambil ujung daftar, tail mengembalikan seluruh isi tanpa elemen pertamanya, sedangkan take dan drop memotong dari arah yang berlawanan, yaitu take mengambil n elemen pertama dan drop membuang n elemen yang sama. Metode join menggabungkan seluruh elemen menjadi satu teks dengan pemisah yang diberikan. Daftar nilai ujian berikut memperlihatkan semuanya sekaligus.
def nilai = [78, 85, 90, 65, 72]
println "first : ${nilai.first()}"
println "last : ${nilai.last()}"
println "tail : ${nilai.tail()}"
println "take 3: ${nilai.take(3)}"
println "drop 3: ${nilai.drop(3)}"
println "join : ${nilai.join('-')}"first : 78
last : 72
tail : [85, 90, 65, 72]
take 3: [78, 85, 90]
drop 3: [65, 72]
join : 78-85-90-65-72Metode-metode itu hanya mengiris tanpa mengubah daftar aslinya, sehingga statistik seperti nilai tertinggi atau rata-rata tetap dapat dihitung ulang dari sumber yang sama. Pada map, collectEntries mengubah daftar menjadi map melalui closure yang menghasilkan pasangan kunci dan nilai, sedangkan subMap mengambil sebagian isi map berdasarkan daftar kunci yang diminta.
def nama = ['ali', 'budi', 'citra']
println "collectEntries: ${nama.collectEntries { [it, it.size()] }}"
def stok = [pensil: 12, penghapus: 8, buku: 5]
println "subMap dua kunci : ${stok.subMap(['penghapus', 'buku'])}"
println "subMap kunci asing: ${stok.subMap(['pensil', 'penggaris'])}"collectEntries: [ali:3, budi:4, citra:5]
subMap dua kunci : [penghapus:8, buku:5]
subMap kunci asing: [pensil:12]Urutan hasil subMap mengikuti urutan permintaannya, bukan urutan map asalnya, sebagaimana terlihat pada penghapus dan buku. Kunci yang diminta tetapi tidak ada di map asal, seperti penggaris pada baris terakhir, tidak ikut muncul di hasil. Map asalnya sendiri tidak berubah karena subMap hanya membaca isi yang diminta.
Berkas: text, readLines, dan eachLine#
Kelas java.io.File mendapat sekelompok metode ekstensi yang membuat pembacaan teks ringkas. Properti text membaca seluruh isi berkas sebagai satu teks, readLines mengembalikan setiap barisnya sebagai List<String>, sedangkan eachLine menjalankan closure per baris dengan nomor baris sebagai argumen kedua. Penulisan isi berkas memakai jalur yang sama, yaitu penugasan ke properti text. Pembukaan dan penutupan sumber data diurus metodenya sendiri, sehingga blok finally untuk menutup resource tidak perlu ditulis. Pembacaan dan penulisan berkas yang lebih lengkap, termasuk pengelolaan resource lanjutan, dibahas pada modul data dan berkas di jalur ini.
// baca-arsip.groovy: membaca berkas teks dengan metode ekstensi File
def arsip = File.createTempFile('piket', '.txt')
arsip.text = 'Ali\nBudi\nCitra'
println "Isi berkas:\n${arsip.text}"
println "readLines: ${arsip.readLines()}"
arsip.eachLine { baris, nomor -> println "$nomor: $baris" }
arsip.delete()Isi berkas:
Ali
Budi
Citra
readLines: [Ali, Budi, Citra]
1: Ali
2: Budi
3: CitraSkrip di atas membuat berkas sementara, mengisinya dengan tiga nama piket, lalu membacanya kembali dengan tiga cara. Hasil readLines berupa list yang siap diproses metode koleksi lain, sedangkan eachLine memberi nomor baris mulai dari 1 sehingga perhitungan manual tidak diperlukan. Resource ditutup oleh metode ekstensinya sendiri, dan berkas sementara dihapus di akhir skrip.
Cara menemukan metode ekstensi#
Dengan lebih dari 2000 metode, persoalannya bukan menghafal, melainkan menemukan. Panduan resmi Apache Groovy mengelompokkan metode ekstensi pada bab bertema kerja, misalnya Working with collections dan Working with IO, lengkap dengan contoh yang bisa dicoba ulang. javadoc DefaultGroovyMethods memuat daftar lengkap metode yang didefinisikan kelas tersebut, dan halaman itu menjadi rujukan paling cepat saat nama metodenya sudah terbayang. Tanda tangan metodenya menyebut kelas penerima sebagai parameter pertama, sehingga kemampuan sebuah tipe dapat ditelusuri dari daftarnya. Di editor dengan dukungan Groovy, fitur autocomplete menampilkan kandidat metode ekstensi setelah titik diketik, biasanya disandingkan dengan metode asli kelas penerima.
Kebiasaan yang paling membawa hasil sebelum menyentuh metaprogramming, yakni kode yang mengubah perilaku program saat berjalan, adalah menjadikan GDK bagian dari rutinitas harian. Kelancaran memakai metode ekstensi adalah keuntungan harian yang paling terasa, karena setiap pemanggilan yang menggantikan kelas bantu tulisan sendiri mengurangi kode yang dirawat. Pemahaman mekanismenya baru perlu diperdalam ketika kebutuhan metaprogramming benar-benar muncul.
Kesalahan umum#
Kesalahan pertama adalah memanggil toInteger pada teks yang belum tentu berisi angka. Metode itu langsung melempar NumberFormatException ketika isinya bukan bilangan, dan skrip berhenti di tengah pekerjaan. Pengamannya berlapis, yaitu memeriksa dengan isNumber atau isInteger terlebih dahulu untuk data yang dapat divalidasi, atau menangkap error dengan try-catch ketika kegagalan memang diperkirakan.
def teks = 'dua'
println "isNumber: ${teks.isNumber()}"
try {
println teks.toInteger()
} catch (NumberFormatException e) {
println "Gagal: ${e.getClass().simpleName}"
}isNumber: false
Gagal: NumberFormatExceptionKeluaran di atas memperlihatkan dua jalur pada data yang sama. Predikat isNumber mengembalikan false untuk teks dua, sehingga konversi dapat dihindari lewat percabangan, sedangkan pemanggilan langsung di dalam try-catch menangkap error dengan rapi. Keduanya dipilih sesuai konteks, yaitu predikat untuk validasi masukan dan try-catch untuk data yang tidak sepenuhnya terkendali.
Kesalahan kedua adalah mengharapkan step mengembalikan daftar nilai sebagaimana metode serupa di bahasa lain. Pada Groovy 5, step hanya memiliki bentuk closure, sehingga pemanggilan tanpa closure gagal dengan MissingMethodException.
try {
println 1.step(3, 1)
} catch (MissingMethodException e) {
println "Gagal: ${e.getClass().simpleName}"
}
def hasil = []
1.step(3, 1) { hasil << it }
println "Bentuk closure: $hasil"Gagal: MissingMethodException
Bentuk closure: [1, 2]Perbaikannya mengumpulkan nilai ke dalam daftar lewat closure, sebagaimana baris terakhir skrip tersebut. Batas awal, batas akhir, dan jarak langkahnya tetap terbaca dari argumen metode, sedangkan penanganan hasilnya pindah ke dalam closure. Bentuk ini sekaligus lebih fleksibel, karena isi closure bisa mencetak, menambah, atau menguji setiap nilai tanpa membuat daftar perantara terlebih dahulu.
Ringkasan#
- Metode ekstensi adalah metode statis kelas bantu GDK yang muncul seolah-olah milik kelas JDK, dengan lebih dari 2000 metode pada 150+ kelas dan sekitar 350 yang baru di Groovy 5, sementara kelas JDK aslinya tidak berubah.
- Kelompok yang paling sering dipakai meliputi Number (
times,upto,downto,step), String (reverse,padLeft,toList,isNumber,isInteger,toInteger,toDouble), list (first,last,tail,take,drop,join), map (collectEntries,subMap), serta berkas (text,readLines,eachLine). - Pada kode dinamis, pemanggilan yang tidak ditemukan di kelas penerima diteruskan melalui MOP ke metode ekstensi, sedangkan kode statis mengikatnya saat kompilasi.
- Dua pengaman harian, yaitu memeriksa
isNumberatauisIntegersebelumtoInteger, dan memakai bentuk closure padastepkarena bentuk keluaran daftar tidak tersedia di Groovy 5.
Referensi#
- Release notes for Groovy 5.0 — Apache Groovy — bagian Extension method additions and improvements memuat angka lebih dari 2000 metode ekstensi pada 150+ kelas JDK beserta 350 metode baru di Groovy 5.
- Groovy Language Documentation — Apache Groovy — panduan resmi yang mengelompokkan metode ekstensi pada bab Working with collections dan Working with IO.
- DefaultGroovyMethods — Apache Groovy — javadoc kelas bantu yang mendefinisikan sebagian besar metode ekstensi GDK.