GString adalah string Groovy yang mendukung interpolasi, yaitu penyisipan nilai variabel dan hasil ekspresi langsung ke dalam teks. Bagian string yang diganti nilai disebut placeholder, dan tanda dolar menjadi penandanya. Topik ini membahas interpolasi pada kutip ganda beserta batas penulisannya, varian penulisan string di Groovy, kelas groovy.lang.GString di balik mekanisme tersebut, waktu evaluasi nilai placeholder, implikasi equals dan kunci map, pemformatan keluaran, serta contoh laporan nilai ujian yang merangkum semuanya. Setelah membacanya, pesan dan laporan berisi data dapat disusun dengan placeholder yang tepat, lengkap dengan pemahaman mengapa dua string yang tampak sama dapat berperilaku berbeda.
Interpolasi pada kutip ganda#
Interpolasi hanya aktif pada string yang diapit tanda kutip ganda. Placeholder paling sederhana adalah $var, nama variabel tunggal yang nilainya disisipkan utuh, sedangkan ${ekspresi} menampung ekspresi apa pun, mulai dari operasi aritmetika sampai pemanggilan metode. Kutip tunggal tidak mengenali kedua bentuk itu, sehingga teks $nama di dalamnya dicetak persis seperti yang ditulis.
Placeholder tanpa kurung kurawal juga mengurai jalur properti berpoin. Penulisan $siswa.kelas.jurusan pada skrip berikut sah dan dibaca sampai properti terdalam, sehingga akses bertingkat pada map tidak memerlukan kurung kurawal. Batasnya ada pada tanda kurung: pemanggilan metode dan operasi lain wajib ditulis di dalam ${}.
// interpolasi-dasar.groovy: interpolasi kutip ganda versus kutip tunggal
def nama = 'Budi'
def nilai = 87
def siswa = [nama: 'Budi', kelas: [tingkat: 'XI', jurusan: 'IPA']]
println "Nama siswa : $nama"
println "Nilai : $nilai"
println "Nilai + 3 : ${nilai + 3}"
println "Huruf kapital: ${nama.toUpperCase()}"
println "Jurusan : $siswa.kelas.jurusan"
println 'Kutip tunggal: $nama tetap apa adanya'Nama siswa : Budi
Nilai : 87
Nilai + 3 : 90
Huruf kapital: BUDI
Jurusan : IPA
Kutip tunggal: $nama tetap apa adanyaEnam baris keluaran di atas menegaskan kontrasnya. Lima baris pertama mengganti placeholder dengan nilai, sedangkan baris terakhir, yang berasal dari kutip tunggal, mencetak $nama apa adanya. Untuk teks yang memang memuat karakter dolar tanpa maksud interpolasi, kutip tunggal menjadi pilihan yang tepat sejak awal. Kebiasaan memilih pembatas secara sadar seperti ini menghindarkan banyak kekeliruan kecil yang sulit dilacak.
Varian penulisan string#
Groovy menyediakan enam bentuk penulisan string dengan aturan interpolasi dan penulisan multi-baris yang berbeda. Kutip tunggal dan kutip ganda berlaku untuk satu baris, sedangkan pasangan triple dan pasangan slashy mampu menampung beberapa baris sekaligus. Skrip berikut menampilkan empat varian lainnya, yaitu triple kutip ganda, triple kutip tunggal, slashy string, dan dollar-slashy string.
def alat = 'sensor'
// triple kutip ganda: multi-baris dengan interpolasi
def laporan = """
Laporan $alat
Status: ${alat.toUpperCase()}
"""
println laporan
// triple kutip tunggal: multi-baris tanpa interpolasi
def templat = '''
Laporan $alat
'''
println templat
// slashy: garis miring terbalik tidak perlu digandakan
def pola = /\d{2}-\d{4}/
println pola
// dollar-slashy: garis miring pola ditulis tanpa escape
def polaTanggal = $/\d{2}/\d{4}/$
println polaTanggal
Laporan sensor
Status: SENSOR
Laporan $alat
\d{2}-\d{4}
\d{2}/\d{4}Keluaran di atas memuat perilaku yang mudah terlewat. String multi-baris menyimpan baris kosong di awal dan di akhir literal, sehingga keluaran println menampilkan baris kosong tambahan di sekitar teksnya. Placeholder $alat pada triple kutip tunggal tidak diganti karena varian ini tidak mendukung interpolasi. Dua varian terakhir dipakai untuk pola ekspresi reguler (regex) karena garis miring terbaliknya tidak perlu digandakan, sedangkan dollar-slashy bahkan menampung garis miring di tengah pola tanpa tanda tambahan. Pemilihan varian mengikuti isinya, bukan kebiasaan, karena setiap pembatas menetapkan aturan penulisannya sendiri.
Tabel berikut merangkum keenam varian beserta aturan utamanya.
| Varian | Pembatas | Interpolasi | Multi-baris | Pemakaian lazim |
|---|---|---|---|---|
| Kutip tunggal | '...' | tidak | tidak | teks biasa tanpa placeholder |
| Kutip ganda | "..." | ya | tidak | teks berisi nilai dinamis |
| Triple kutip tunggal | '''...''' | tidak | ya | teks panjang yang harus tetap literal |
| Triple kutip ganda | """...""" | ya | ya | templat multi-baris berisi nilai |
| Slashy string | /.../ | ya | ya | pola regex tanpa penggandaan \ |
| Dollar-slashy string | $/.../$ | ya | ya | pola yang memuat / atau $ |
Waktu evaluasi nilai placeholder#
Interpolasi menghasilkan objek kelas groovy.lang.GString, yaitu kelas yang menyimpan potongan teks bersama daftar nilai placeholder-nya. Nilai $var dan ${ekspresi} dihitung pada saat objek GString dibuat, bukan ketika string dicetak. Akibatnya, perubahan variabel setelah GString dibuat tidak memengaruhi isinya.
Groovy menyediakan satu pengecualian untuk kebutuhan evaluasi terlambat. Placeholder berbentuk ${-> ekspresi} menyimpan closure, yaitu blok kode yang baru dijalankan ketika GString diubah menjadi String, misalnya saat dicetak. Dengan bentuk ini, isi GString mengambil nilai terkini pada saat dibaca, bukan nilai yang ada saat pembuatannya.
def nama = 'Ali'
// nilai placeholder diambil saat GString dibuat
def sapaan = "Halo, $nama"
nama = 'Budi'
println sapaan
// closure placeholder dievaluasi saat GString dibaca
def sapaanTunda = "Halo, ${-> nama}"
nama = 'Citra'
println sapaanTunda
println "Sapaan berupa GString: ${sapaan instanceof groovy.lang.GString}"Halo, Ali
Halo, Citra
Sapaan berupa GString: trueDua baris pertama keluaran menegaskan perbedaannya. String sapaan tetap berisi Halo, Ali karena nilainya sudah terkunci saat dibuat, sedangkan sapaanTunda berisi Halo, Citra karena closure-nya baru dijalankan setelah nama berubah. Baris terakhir memastikan bahwa hasil interpolasi memang objek groovy.lang.GString, dan implementasi konkretnya dihasilkan kompilator dengan nama kelas GStringImpl.
Equals dan kunci map#
GString dan String tidak pernah dianggap sama oleh metode equals, meskipun teks yang dihasilkannya identik. Operator == pada Groovy membandingkan nilai teks kedua operannya, bukan tipe kelasnya, sehingga GString dan String yang berteks sama tetap dianggap sama. Perbedaan baru terasa ketika kode melewati equals dan hashCode, yaitu jalur yang dipakai kelas koleksi seperti HashMap dan HashSet.
def nama = 'Dewi'
def gs = "Halo, $nama"
def s = 'Halo, Dewi'
println "Isi identik : ${gs.toString() == s}"
println "Operator == : ${gs == s}"
println "equals() dua arah : ${gs.equals(s)} / ${s.equals(gs)}"
println "hashCode() sama : ${gs.hashCode() == s.hashCode()}"
def daftar = ['Halo, Dewi']
println "contains(gs) : ${daftar.contains(gs)}"Isi identik : true
Operator == : true
equals() dua arah : false / false
hashCode() sama : false
contains(gs) : falseKeluaran di atas merangkum keempat perilakunya. Isi yang identik diakui oleh ==, tetapi equals dua arah tetap false dan nilai hashCode keduanya berbeda. Konsekuensinya paling jelas pada kunci map, karena pencocokan kunci map memakai hashCode dan equals. Karena itu, dua kunci yang teksnya sama tetapi bertipe GString dan String menjadi dua entri.
def hari = 'Senin'
def label = "piket $hari"
// GString dan String yang tampak sama menjadi dua kunci berbeda
def jadwal = [(label): 'Ali', 'piket Senin': 'Budi']
println "Jumlah entri : ${jadwal.size()}"
println "Tipe kunci : ${jadwal.keySet()*.getClass().simpleName}"
// perbaikan pertama: satukan tipe kunci lewat toString
def jadwalRapi = [(label.toString()): 'Ali', 'piket Senin': 'Budi']
println "Entri setelah toString: ${jadwalRapi.size()}"
// perbaikan kedua: subscript otomatis mengonversi GString menjadi String
def piket = [:]
piket[label] = 'Citra'
println "Tipe kunci subscript : ${piket.keySet()*.getClass().simpleName}"
println "Ambil dengan String : ${piket['piket Senin']}"Jumlah entri : 2
Tipe kunci : [GStringImpl, String]
Entri setelah toString: 1
Tipe kunci subscript : [String]
Ambil dengan String : CitraPada map jadwal, kunci (label) berupa GString sedangkan kunci kedua berupa String, sehingga jumlah entrinya dua. Konversi label.toString() menyatukan tipe kuncinya, dan map jadwalRapi hanya berisi satu entri. Pada map piket, penugasan lewat kurung siku (subscript) mengonversi kunci GString menjadi String secara otomatis, sehingga pencarian dengan kunci String menemukan nilainya.
Pemformatan dan karakter dolar#
Ekspresi di dalam kurung kurawal boleh memanggil String.format, sehingga pengaturan jumlah desimal, lebar kolom, dan perataan dikerjakan langsung di dalam placeholder. Kode format %.1f menampilkan satu angka di belakang tanda desimal, sedangkan %-6s meratakan teks ke kiri dalam lebar enam karakter. Dengan begitu, logika perhitungan dan logika tampilan tetap terpisah tanpa variabel perantara.
Karakter dolar yang ingin tampil sebagai teks wajib ditulis \$. Dolar yang tidak diikuti nama variabel atau kurung kurawal tidak dibiarkan begitu saja, melainkan menggagalkan kompilasi karena parser tidak menemukan placeholder yang sah. Penulisan \$ ini disebut escape, yaitu penandaan agar karakter berikutnya dibaca secara literal.
def nilai = [78, 85, 90]
def rataRata = nilai.sum() / nilai.size()
def nama = 'Ali'
println "Rata-rata : ${String.format('%.1f', rataRata)}"
println "Baris berformat : ${String.format('| %-6s | %3d |', nama, 87)}"
println "Tanda \$ di awal prompt shell bukan placeholder"Rata-rata : 84.3
Baris berformat : | Ali | 87 |
Tanda $ di awal prompt shell bukan placeholderBaris pertama menghitung rata-rata dan langsung memformatnya menjadi satu desimal di dalam placeholder. Baris kedua memformat label dan nilai menjadi kolom berlebar tetap, sedangkan baris ketiga menampilkan dolar secara literal karena ditulis \$. Pola String.format di dalam placeholder seperti inilah yang dipakai contoh terpadu di bawah.
Contoh terpadu: laporan nilai ujian#
Seluruh teknik di atas terlihat utuh pada laporan nilai ujian berikut. Skrip menyimpan data peserta sebagai list berisi map, satu map untuk setiap peserta dengan kunci nama dan nilai. Dari data itu, skrip menghitung rata-rata kelas, memilih pemilik nilai tertinggi, menentukan predikat, lalu menyusun laporan yang rapi.
// laporan-nilai.groovy: laporan nilai ujian dengan GString
def ujian = [
[nama: 'Ali', nilai: 82],
[nama: 'Budi', nilai: 74],
[nama: 'Citra', nilai: 91],
]
def rata = ujian*.nilai.sum() / ujian.size()
def terbaik = ujian.max { it.nilai }
println "LAPORAN NILAI UJIAN"
println "==================="
ujian.each { s ->
def predikat = s.nilai >= 80 ? 'A' : 'B'
println "${s.nama.padRight(8)}: ${s.nilai} (predikat $predikat)"
}
println "==================="
println "Jumlah peserta : ${ujian.size()}"
println "Rata-rata : ${String.format('%.1f', rata)}"
println "Nilai terbaik : $terbaik.nama ($terbaik.nilai)"LAPORAN NILAI UJIAN
===================
Ali : 82 (predikat A)
Budi : 74 (predikat B)
Citra : 91 (predikat A)
===================
Jumlah peserta : 3
Rata-rata : 82.3
Nilai terbaik : Citra (91)# tidak-dijalankan
# menjalankan laporan dari folder tempat berkasnya disimpan
groovy laporan-nilai.groovyTiga baris pertama laporan memakai placeholder variabel dan metode padRight agar titik dua tersusun rata. Predikat ditentukan ekspresi ternary, percabangan yang memadatkan dua kemungkinan dalam satu ekspresi, di dalam placeholder yang membandingkan nilai dengan batas 80. Rata-rata dihitung lewat operator spread *.nilai, yaitu pengambilan satu properti dari setiap elemen list, lalu diformat %.1f di dalam placeholder. Baris terakhir memanfaatkan jalur properti $terbaik.nama dan $terbaik.nilai sehingga data pemilik nilai tertinggi tersusun tanpa kurung kurawal.
Kesalahan umum#
Kesalahan pertama adalah memanggil metode langsung pada placeholder, misalnya $kata.length(). GString hanya mengurai placeholder tanpa kurung kurawal sebagai nama variabel dan jalur properti berpoin, sehingga tanda kurung di belakangnya diperlakukan sebagai teks. Pencarian properti bernama length pada String gagal dan melempar MissingPropertyException.
def kata = 'contoh'
try {
println "Panjang: $kata.length()"
} catch (MissingPropertyException e) {
println "Gagal: ${e.getClass().simpleName}"
}
println "Benar: ${kata.length()}"Gagal: MissingPropertyException
Benar: 6Perbaikannya menarik seluruh ekspresi ke dalam kurung kurawal, yaitu ${kata.length()}, sebagaimana baris terakhir skrip. Di dalam ${}, tanda kurung metode ikut menjadi bagian ekspresi yang dievaluasi, bukan teks yang menyusul placeholder. Aturan praktisnya pendek: $var dan $obj.prop boleh tanpa kurung kurawal, sedangkan bentuk lain masuk ${}.
Kesalahan kedua adalah menganggap GString dan String saling menggantikan di semua situasi. Perbandingan dengan == memang bernilai true, tetapi equals dan hashCode membedakannya, sehingga pencarian map dan operasi koleksi berbasis equals gagal menemukan data. Konversi dengan toString() sebelum nilai masuk ke struktur data atau API yang sensitif terhadap tipe menjadi kebiasaan yang aman.
Ringkasan#
- Interpolasi aktif pada kutip ganda, triple kutip ganda, slashy string, dan dollar-slashy string dengan placeholder
$var, jalur properti$obj.prop, dan ekspresi${ekspresi}, sedangkan kutip tunggal dan triple kutip tunggal mencetak teks apa adanya. - Interpolasi menghasilkan objek
groovy.lang.GString, dan nilai placeholder$varserta${ekspresi}diambil saat GString dibuat, sedangkan placeholder closure${-> ekspresi}baru dievaluasi saat GString dibaca. - GString dan String tidak pernah sama menurut equals dan hashCode, sehingga kunci map berupa GString tidak menemukan entri berkunci String. Solusinya
toString()atau penugasan subscript yang otomatis mengonversi. - Ekspresi apa pun termasuk
String.formatdapat ditulis di dalam${}, dan dolar literal ditulis\$karena dolar yang tidak di-escape menggagalkan kompilasi.
Referensi#
- String syntax — Apache Groovy — dokumentasi resmi sintaks string Groovy, mencakup kutip tunggal dan ganda, triple-quoted, slashy, dan dollar-slashy beserta aturan interpolasinya.
- Operators — Apache Groovy
— dokumentasi resmi operator Groovy, termasuk operator pola
~yang lazim dipasangkan dengan slashy dan dollar-slashy string.