Topik ini membahas kompilasi statis pada Groovy, yaitu kemampuan kompilator memeriksa tipe dan menyalurkan panggilan metode saat kompilasi seperti pada Java. Pembahasan mencakup tiga tingkat pemeriksaan yang tersedia, mekanisme flow typing untuk variabel yang bertipe berubah, bentuk pesan error pemeriksaan statis, serta anotasi yang membuka kembali blok dinamis di tengah kode statis. Setiap perilaku didemonstrasikan dengan berkas yang dijalankan nyata, termasuk satu demo error yang keluarannya disalin persis, sehingga perbedaan ketiga tingkat dapat ditelusuri sampai ke teks pesannya.
Tiga tingkat pemeriksaan tipe#
Secara default, kode Groovy sepenuhnya dinamis, yaitu pemeriksaan tipe tidak dilakukan dan setiap panggilan metode disalurkan saat program berjalan melalui metakelas. Kebalikannya disediakan oleh dua anotasi (annotation) pada paket groovy.transform, yaitu metadata yang ditempelkan pada kelas atau metode untuk mengubah perilaku kompilator. Anotasi @TypeChecked menambahkan pemeriksaan tipe saat kompilasi tanpa mengubah cara panggilan disalurkan, sedangkan @CompileStatic menambahkan pemeriksaan tersebut sekaligus menghasilkan kode panggilan langsung seperti milik Java.
Kedua anotasi bekerja pada batas kelas atau metode yang ditandainya, sehingga satu berkas dapat memuat bagian dinamis dan bagian statis sekaligus. Tabel berikut merangkum perbedaan ketiga tingkat.
| Aspek | Tanpa anotasi | @TypeChecked | @CompileStatic |
|---|---|---|---|
| Pemeriksaan tipe saat kompilasi | tidak ada | ada | ada |
| Penyaluran panggilan | runtime | runtime | saat kompilasi |
| Hook MOP dan metakelas | berlaku penuh | tetap berlaku | tidak terlihat dari kode statis |
| Biaya panggilan saat runtime | pencarian metakelas | sama dengan dinamis | panggilan langsung seperti Java |
Pemilihan tingkatnya mengikuti peran berkas yang ditulis. Skrip kecil yang memanfaatkan hook MOP tetap dinamis, sedangkan pustaka yang akan dipakai ulang memperoleh keandalan dari pemeriksaan statis. Pemeriksaan statis menangkap salah ketik nama metode dan kesalahan tipe sebelum program dijalankan, yang pada kode dinamis baru terlihat sebagai MissingMethodException di tengah eksekusi.
Flow typing pada variabel def#
Pemeriksaan statis tidak menuntut tipe yang ditulis eksplisit di setiap baris. Mekanisme flow typing memungkinkan variabel def berganti tipe mengikuti penugasannya, dan kompilator mengikuti perubahan tersebut dari baris ke baris. Setiap panggilan diperiksa terhadap tipe yang berlaku pada titik pemanggilannya, bukan terhadap tipe pertama yang pernah dipegang variabel itu. Dengan demikian, gaya penulisan Groovy yang mengandalkan def tetap dapat diperiksa tanpa diubah menjadi deklarasi tipe yang panjang.
// tidak-dijalankan
import groovy.transform.TypeChecked
@TypeChecked
String alurTipe() {
def x = 'belajar' // x bertipe String
int panjang = x.length() // sah: String punya length()
x = 42 // x kini bertipe Integer
int berikut = x + 1 // sah: Integer + Integer
x = [1, 2, 3] // x kini bertipe List
int banyak = x.size() // sah: List punya size()
return "${panjang} ${berikut} ${banyak}"
}
println alurTipe()# menjalankan demo flow typing
groovy alur-tipe.groovy7 43 3Keluaran tersebut memuat tiga nilai dari variabel x yang sama pada tiga titik berbeda, yaitu panjang teks belajar sebesar 7, hasil 42 + 1 sebesar 43, dan jumlah elemen daftar sebanyak 3. Variabel x berganti tipe dari String ke Integer lalu ke List, dan ketiga panggilan length(), penjumlahan, serta size() tetap lolos karena diperiksa terhadap tipe yang berlaku di setiap barisnya. Tanpa flow typing, ketiga perubahan tipe ini harus ditulis sebagai tiga variabel berbeda dengan tipe eksplisit.
Pesan error pemeriksaan statis#
Pemeriksaan statis menolak kode saat kompilasi apabila ada panggilan yang tidak dapat dipastikan keberadaannya. Berkas berikut sengaja memuat salah ketik, yaitu lenght() yang seharusnya length(), pada metode yang diberi anotasi @TypeChecked. Kode ini tidak pernah sampai dieksekusi, karena kompilator berhenti sebelum program berjalan.
// tidak-dijalankan
import groovy.transform.TypeChecked
@TypeChecked
int panjang(String teks) {
return teks.lenght() // salah ketik: seharusnya length
}
println panjang('halo')# menjalankan demo error pemeriksaan statis
groovy cek-lenght.groovyorg.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
/home/kal/my-project/content/groovy/objek-dan-mop/kompilasi-statis/cek-lenght.groovy: 6: [Static type checking] - Cannot find matching method java.lang.String#lenght(). Please check if the declared type is correct and if the method exists.
@ line 6, column 12.
return teks.lenght() // salah ketik: seharusnya length
^
1 errorPesan [Static type checking] menyebutkan baris dan kolom yang bermasalah, yaitu baris 6 kolom 12, disertai metode yang tidak ditemukan beserta tipenya, yaitu java.lang.String#lenght(). Baris pertama keluaran memuat jalur berkas pada sandbox tempat eksekusi dilakukan, dan jalur tersebut mengikuti tempat berkas disimpan saat dijalankan ulang. Pada kode dinamis tanpa anotasi, salah ketik yang sama baru meledak sebagai MissingMethodException saat program berjalan dan barisnya dieksekusi. Perbedaan titik kegagalan inilah keuntungan utama pemeriksaan statis, karena kesalahan tertangkap sebelum kode dipakai.
Pemeriksaan tanpa mengubah penyaluran#
Anotasi @TypeChecked hanya menambahkan pemeriksaan, dan penyaluran panggilan tetap dinamis sebagaimana kode tanpa anotasi. Kompilator memverifikasi deklarasi dan panggilan yang dapat dipastikan, tetapi tidak menyisipkan instruksi panggilan langsung pada kode hasil kompilasi, sehingga jalan eksekusinya identik dengan kode dinamis. Konsekuensinya, hook MOP tetap bekerja pada kode yang diperiksa secara statis, selama pemanggilannya sendiri dilakukan dari bagian program yang dinamis.
// tidak-dijalankan
import groovy.transform.TypeChecked
// @TypeChecked hanya memeriksa; penyaluran panggilan tetap dinamis
@TypeChecked
class LaporanCek {
def methodMissing(String nama, Object args) {
return "hook menerima ${nama}"
}
}
// pemanggilan dilakukan dari kode dinamis di luar kelas
def l = new LaporanCek()
println l.sudahAda()# menjalankan demo pemeriksaan tanpa perubahan penyaluran
groovy dinamis-tetap.groovyhook menerima sudahAdaPanggilan l.sudahAda() menyasar metode yang tidak ada, dan hook methodMissing di dalam kelas tetap menerima panggilan itu meskipun kelasnya beranotasi @TypeChecked. Baris pemanggilnya berada di luar kode yang diperiksa, sehingga penyalurannya dinamis dan metakelas tetap berkewajiban mencari jawaban. Perilaku ini menegaskan posisi anotasi tersebut sebagai pemeriksa keandalan, bukan sebagai pengubah cara program dieksekusi. Kelas yang hidup dari hook MOP tetap dapat memperoleh pemeriksaan atas bagian kelasnya yang berupa kode biasa, dan pemanggilan dinamisnya cukup ditempatkan di luar lingkup anotasi. Pembagian semacam ini membuat pemeriksaan statis dapat diterapkan bertahap tanpa menonaktifkan perilaku dinamis yang memang diinginkan.
@CompileDynamic dan pembagian produk#
Anotasi @CompileDynamic adalah kebalikan dari dua anotasi sebelumnya, yaitu membebaskan satu metode atau satu blok kode dari pemeriksaan statis dan mengembalikan penyalurannya ke jalur dinamis. Anotasi ini dipakai pada bagian kecil yang memang membutuhkan sifat dinamis, misalnya panggilan ke metode yang ditempelkan lewat ExpandoMetaClass, karena metode semacam itu tidak terlihat oleh pemeriksa statis.
// tidak-dijalankan
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
class Kotak {
String isi
}
// metode ditambahkan lewat MOP, tidak terlihat oleh pemeriksa statis
Kotak.metaClass.deskripsi << { -> "Kotak berisi ${delegate.isi}" }
@CompileDynamic
String ambilDeskripsi(Kotak k) {
return k.deskripsi()
}
@CompileStatic
String tampilkan(Kotak k) {
return ambilDeskripsi(k)
}
println tampilkan(new Kotak(isi: 'surat lama'))# menjalankan demo pembukaan blok dinamis
groovy buka-dinamis.groovyKotak berisi surat lamaMetode deskripsi hanya ada pada metakelas, sehingga panggilannya hanya sah di dalam ambilDeskripsi yang diberi anotasi @CompileDynamic. Metode tampilkan yang diperiksa secara statis memanggil ambilDeskripsi secara normal, karena metode tersebut adalah metode biasa yang ditetapkan saat kompilasi. Keluaran Kotak berisi surat lama membuktikan bahwa kode statis dan blok dinamis dapat disambung tanpa saling meniadakan.
Pemisahan ini membawa satu kesimpulan praktis. Skrip dinamis dan pustaka yang dikompilasi statis adalah dua produk yang berbeda, dengan jaminan dan biayanya masing-masing. Skrip dinamis unggul dalam kelenturan dan kedekatan dengan hook MOP, sedangkan pustaka statis unggul dalam keandalan dan kecepatan panggilan. Menggabungkan keduanya sah dilakukan pada sambungan yang sengaja ditandai, dan bukan pada asumsi bahwa kedua produk itu dapat saling dipertukarkan tanpa konsekuensi.
Ringkasan#
@TypeCheckedmenambahkan pemeriksaan tipe saat kompilasi tanpa mengubah penyaluran dinamis, sedangkan@CompileStaticmenghasilkan panggilan langsung seperti Java.- Flow typing mengikuti perubahan tipe variabel
defdari baris ke baris, sehingga pemeriksaan tetap sah pada setiap titik pemanggilan. - Error pemeriksaan statis berupa pesan
[Static type checking]saat kompilasi, alih-alihMissingMethodExceptiondi tengah eksekusi. @CompileDynamicmembuka blok dinamis di tengah kode statis, dan skrip dinamis dengan pustaka statis tetap merupakan dua produk yang berbeda.
Referensi#
- TypeChecked (groovy.transform) — Apache Groovy — API anotasi pemeriksaan tipe beserta fase pemeriksaannya.
- CompileStatic (groovy.transform) — Apache Groovy — API anotasi kompilasi statis dan tingkat ketat pemeriksaannya.
- CompileDynamic (groovy.transform) — Apache Groovy — API anotasi pembuka blok dinamis di dalam kode statis.
- Metaprogramming — Apache Groovy — Panduan resmi hook MOP yang menjadi kontras pemeriksaan statis.