Range dan alur kendali

Menggunakan range, perulangan each dan times, serta switch yang berperan sebagai ekspresi.

Daftar isi

Groovy menyediakan range sebagai objek siap pakai, metode perulangan yang ringkas, dan percabangan yang dapat berperan sebagai pernyataan maupun ekspresi. Topik ini mempraktikkan ketiganya pada data sederhana, mulai dari membentuk range inklusif, eksklusif, mundur, dan berbasis karakter, mengambil potongan list dengan subscript range, mengulang kerja dengan keluarga each serta metode Number, mengatur keputusan dengan if/else, ternary, dan Elvis, sampai menulis switch dengan case berbentuk nilai, range, koleksi, kelas, dan regex. Setelah membacanya, alur kendali sebuah skrip Groovy dapat disusun dengan penulisan yang jauh lebih ringkas dibanding perulangan berindeks klasik.

Range sebagai objek#

Range adalah objek yang mewakili urutan nilai di antara dua batas dan ditulis dengan operator .. untuk batas inklusif atau ..< untuk batas eksklusif. Batas yang ditukar posisinya menghasilkan urutan menurun, sedangkan pasangan karakter seperti 'a'..'e' membentuk urutan menaik mengikuti nilai karakternya. Jadwal piket empat hari atau nomor tiket satu sampai lima pun cukup ditulis sebagai satu range, bukan daftar yang diketik satu per satu. Karena berstatus objek, range membawa properti from dan to, metode size dan contains, serta metode step yang mengambil elemen dengan jarak tertentu.

groovy range-dasar.groovy
// range-dasar.groovy: bentuk range dan propertinya
def hitung = 1..5
def eksklusif = 1..<5
def mundur = 5..1
def huruf = 'a'..'e'

println "Inklusif   : ${hitung.toList()}"
println "Eksklusif  : ${eksklusif.toList()}"
println "Mundur     : ${mundur.toList()}"
println "Huruf      : ${huruf.toList()}"
println "from/to    : ${hitung.from} sampai ${hitung.to}"
println "size       : ${hitung.size()}"
println "contains 3 : ${hitung.contains(3)}"
println "contains 9 : ${hitung.contains(9)}"
println "step 2     : ${hitung.step(2)}"
text Keluaran
Inklusif   : [1, 2, 3, 4, 5]
Eksklusif  : [1, 2, 3, 4]
Mundur     : [5, 4, 3, 2, 1]
Huruf      : [a, b, c, d, e]
from/to    : 1 sampai 5
size       : 5
contains 3 : true
contains 9 : false
step 2     : [1, 3, 5]

Batas 1..<5 menyingkirkan angka 5 dari hasilnya, sedangkan 5..1 langsung membentuk urutan menurun tanpa perlu membalik apa pun. Properti from dan to memperlihatkan kedua batas aslinya, sedangkan step(2) mengambil satu elemen lalu melompati satu elemen berikutnya. Langkah pada step wajib berupa bilangan bulat karena tanda tangannya hanya step(int), sehingga penulisan step(0.5) memicu MissingMethodException.

Subscript list dengan range#

Subscript dengan range mengambil potongan list dalam satu penulisan kurung siku. Indeks negatif menghitung posisi dari belakang, range mundur membalik urutan potongan, dan batas ..< mencegah indeks terakhir ikut terpotong. Teknik ini lazim dipakai untuk memotong antrean tiket, misalnya mengambil sepuluh nomor pertama atau membalik urutannya untuk panggilan dari belakang. Karena rentangnya ditulis eksplisit, range yang melewati indeks terakhir list akan melempar IndexOutOfBoundsException, bukan diam-diam memotong hasilnya.

groovy Contoh: subscript list dengan range
def antrean = ['Ali', 'Budi', 'Citra', 'Dewi', 'Eka']

println antrean[0..2]
println antrean[2..<5]
println antrean[3..1]
println antrean[-2..-1]

try {
    println antrean[3..9]
} catch (IndexOutOfBoundsException e) {
    println "Gagal: ${e.getClass().simpleName}"
}
text Keluaran
[Ali, Budi, Citra]
[Citra, Dewi, Eka]
[Dewi, Citra, Budi]
[Dewi, Eka]
Gagal: IndexOutOfBoundsException

Potongan antrean[0..2] memuat tiga elemen pertama, sedangkan antrean[3..1] membaca dari indeks tiga turun ke satu sehingga urutannya terbalik. Indeks negatif -2..-1 mengambil dua elemen terakhir tanpa perlu mengetahui panjang list. Pemanggilan antrean[3..9] melampaui indeks terakhir dan berakhir pada IndexOutOfBoundsException, sama seperti subscript satu indeks yang di luar batas. Hasil potongan berupa list baru, sehingga perubahan pada potongan tidak memengaruhi list asalnya. Untuk range yang batasnya dinamis, kebiasaan yang aman adalah menguji size list lebih dahulu sebelum potongan dibentuk.

Perulangan dengan each dan metode Number#

Groovy menggantikan perulangan berindeks dengan closure, yaitu blok kode yang dikirim sebagai argumen metode. Metode each memanggil closure untuk setiap elemen range menggunakan parameter implisit it atau parameter bernama, sedangkan variannya eachWithIndex menyertakan indeks mulai dari nol. Parameter bernama seperti hari pada contoh membuat maksud tiap putaran terbaca tanpa perlu menafsirkan it. Untuk jumlah putaran yang tidak menempel pada range, metode times, upto, downto, dan step milik Number menyediakan pola perulangan yang sama.

groovy perulangan.groovy
// perulangan.groovy: keluarga each dan metode perulangan Number
(1..3).each { hari ->
    println "Piket hari ke-$hari"
}

(10..<13).each {
    println "Nomor tiket $it"
}

(1..3).eachWithIndex { nomor, urut ->
    println "Antrean ${urut + 1}: nomor $nomor"
}

3.times {
    println "times: ulangan ke-$it"
}

1.upto(3) {
    println "upto: $it"
}

3.downto(1) {
    println "downto: $it"
}

(1..7).step(2) {
    println "step: $it"
}
text Keluaran
Piket hari ke-1
Piket hari ke-2
Piket hari ke-3
Nomor tiket 10
Nomor tiket 11
Nomor tiket 12
Antrean 1: nomor 1
Antrean 2: nomor 2
Antrean 3: nomor 3
times: ulangan ke-0
times: ulangan ke-1
times: ulangan ke-2
upto: 1
upto: 2
upto: 3
downto: 3
downto: 2
downto: 1
step: 1
step: 3
step: 5
step: 7

Closure eachWithIndex menerima pasangan nilai dan indeks, sehingga nomor tiket dan urutan pemanggilannya tersusun bersamaan. Metode times menghitung dari nol, upto menaik menuju batas atas, dan downto menurun dari nilai awalnya. Penulisan (1..7).step(2) { } merupakan bentuk closure dari step, sedangkan step(2) tanpa closure mengembalikan list yang siap dipakai di tempat lain. Seluruh metode di atas menyingkirkan mekanisme indeks dan menyisakan niat programnya, misalnya jadwal piket atau antrean yang dihitung ulang setiap putaran.

Perulangan for dengan break dan continue#

Perulangan for dengan kata in menelusuri range atau koleksi apa pun dengan bentuk umum seperti for (i in 0..3). Bentuk ini menjadi tempat yang tepat ketika putaran perlu berhenti lebih awal atau melewati satu nilai tertentu. Pernyataan break menghentikan seluruh perulangan, sedangkan continue melompat ke putaran berikutnya, dan keduanya hanya sah pada perulangan berbentuk pernyataan, bukan di dalam closure each. Batas range yang ditelusuri for tidak harus tetap, sehingga bentuk seperti for (n in 0..jumlahTiket) lazim dipakai ketika batasnya baru diketahui saat program berjalan.

groovy Contoh: break dan continue pada perulangan for
def dipanggil = []
for (i in 1..5) {
    if (i == 2) {
        continue
    }
    dipanggil << i
    if (i == 4) {
        break
    }
}
println "Nomor yang dipanggil: $dipanggil"

for (huruf in 'a'..'c') {
    println "Loket $huruf buka"
}
text Keluaran
Nomor yang dipanggil: [1, 3, 4]
Loket a buka
Loket b buka
Loket c buka

Nomor dua dilewati oleh continue karena belum datang, sedangkan break menghentikan pemanggilan setelah nomor empat sehingga list hanya berisi [1, 3, 4]. Baris terakhir menelusuri range karakter 'a'..'c' yang berperan sebagai kode loket. Keluaran itu menunjukkan kedua pernyataan bekerja pada mekanisme perulangan itu sendiri, bukan pada closure yang hanya dipanggil per elemen. Bila kebutuhannya hanya menemukan satu elemen yang cocok, metode find menggantikan pola break ini dengan cara yang lebih ringkas.

Percabangan if, ternary, dan Elvis#

Percabangan if/else menilai kondisi lalu menjalankan blok yang sesuai, sedangkan operator ternary ? : memadatkan dua kemungkinan ke dalam satu ekspresi. Varian Groovy bernama operator Elvis ?: memperpendeknya lagi, yaitu memakai nilai di sebelah kiri ketika nilainya dianggap true dan menggantinya dengan nilai di sebelah kanan ketika false. Bentuk Elvis menjadi fallback singkat untuk data yang mungkin kosong, seperti catatan atau nama peserta yang belum diisi. Ketiga bentuk dapat dipakai berdampingan karena keputusan yang panjang tetap ditulis if/else, sedangkan keputusan satu baris pindah ke ternary atau Elvis.

groovy Contoh: if/else, ternary, dan Elvis
def nilai = 82

if (nilai >= 85) {
    println "Predikat A"
} else if (nilai >= 75) {
    println "Predikat B"
} else {
    println "Predikat C"
}

def status = nilai >= 75 ? 'lulus' : 'remedial'
println "Status   : $status"

def catatan = ''
println "Catatan  : ${catatan ?: '(kosong)'}"

def nama = null
println "Nama     : ${nama ?: 'Tanpa Nama'}"
println "Operator : ${'Citra' ?: 'Tanpa Nama'}"
text Keluaran
Predikat B
Status   : lulus
Catatan  : (kosong)
Nama     : Tanpa Nama
Operator : Citra

Nilai 82 jatuh pada cabang nilai >= 75, sehingga predikatnya B dan statusnya lulus menurut ternary. String kosong dan null sama-sama dianggap false oleh Elvis, sehingga keduanya digantikan teks penggantinya, sedangkan 'Citra' lolos tanpa perubahan. Aturan benar-salah ini disebut Groovy truth, yaitu keputusan false untuk nilai null, angka nol, dan nilai kosong seperti string kosong, di luar ekspresi perbandingan eksplisit.

Switch sebagai pernyataan#

Switch Groovy mencocokkan satu nilai terhadap daftar case dengan aturan isCase, sehingga case-nya tidak terbatas pada konstanta seperti pada bahasa turunan C. Nilai tunggal diuji dengan kesetaraan, range dengan keanggotaan, koleksi dengan kehadiran, kelas dengan instanceof, dan pola regex dengan kecocokan string. Bentuk seperti ini menggantikan rangkaian if/else berlapis ketika satu nilai perlu diurutkan ke dalam banyak kelompok. Urutan case menentukan hasil karena pencocokan berhenti pada case pertama yang cocok.

groovy switch-dasar.groovy
// switch-dasar.groovy: beragam bentuk case pada switch pernyataan
def klasifikasi(nilai) {
    switch (nilai) {
        case 100:
            return 'sempurna'
        case 80..99:
            return 'sangat baik'
        case ['A', 'B', 'C']:
            return "kelas $nilai"
        case Integer:
            return "bilangan bulat $nilai"
        case ~/L[0-9]+/:
            return "kode loket $nilai"
        default:
            return 'tidak dikenal'
    }
}

println klasifikasi(100)
println klasifikasi(85)
println klasifikasi('B')
println klasifikasi(7)
println klasifikasi('L12')
println klasifikasi('x9')
text Keluaran
sempurna
sangat baik
kelas B
bilangan bulat 7
kode loket L12
tidak dikenal

Nilai 100 tertangkap case pertama sebelum sempat diuji oleh range 80..99, sedangkan 7 lolos hingga case Integer karena tidak termasuk keduanya. String 'L12' cocok dengan pola ~/L[0-9]+/, dan 'x9' tidak cocok dengan case mana pun sehingga jatuh ke default. Setiap case di sini ditutup return karena contohnya berada di dalam metode, sedangkan case pada pernyataan switch biasa ditutup break. Tanpa penutup seperti itu, eksekusi jatuh ke badan case berikutnya, sehingga lupa menutup case mudah menghasilkan keluaran yang menyesatkan.

bash
# tidak-dijalankan
# menampilkan skrip contoh yang disertakan pada topik ini
ls -1 *.groovy

Switch sebagai ekspresi#

Sejak Groovy 4, switch juga berperan sebagai ekspresi yang mengembalikan nilai dari cabangnya. Bentuk panah case ... -> menuliskan hasil langsung tanpa break, sedangkan bentuk titik dua mengembalikan nilai melalui kata yield yang wajib ditulis pada setiap cabang. Hasil ekspresi dapat ditugaskan ke variabel atau disisipkan ke placeholder, sehingga pemetaan nilai ke label tersusun dalam satu pernyataan. Bentuk panah dan bentuk yield menghasilkan program yang sama, dan pemilihannya sekadar soal keterbacaan.

groovy Contoh: switch sebagai ekspresi dan hasil null
def loket = { kode ->
    switch (kode) {
        case 'A' -> 1
        case 'B' -> 2
        case ~/[CD]/ -> 3
        default -> 0
    }
}

println "Tiket A: Loket ${loket('A')}"
println "Tiket C: Loket ${loket('C')}"
println "Tiket Z: Loket ${loket('Z')}"

// bentuk titik dua pada switch ekspresi wajib yield
def predikat = switch(85) {
    case 80..100:
        yield 'sangat baik'
    default:
        yield 'cukup'
}
println "Predikat : $predikat"

// tanpa default: tanpa kecocokan hasilnya null
def tipe = switch(42) {
    case 'teks' -> 'string'
    case BigDecimal -> 'desimal'
}
println "Null tanpa kecocokan: ${tipe == null}"
text Keluaran
Tiket A: Loket 1
Tiket C: Loket 3
Tiket Z: Loket 0
Predikat : sangat baik
Null tanpa kecocokan: true

Closure loket mengembalikan nomor loket dari cabang panahnya, dan pola ~/[CD]/ menangkap tiket berawalan C maupun D sekaligus. Bentuk titik dua pada predikat memperlihatkan yield yang tugasnya sama dengan cabang panah, yaitu menyerahkan nilai cabang kepada ekspresi. Pada ekspresi tipe, tidak ada case yang cocok dengan 42 dan default tidak dituliskan, sehingga hasilnya null.

Kesalahan umum#

Kesalahan pertama adalah merangkai each pada range mundur tanpa kurung. Penulisan 5..1.each { } dibaca 5..(1.each { }), yaitu closure menempel pada angka 1 dan dijalankan satu kali, sedangkan range 5..1 yang tersisa tidak pernah ditelusuri dan tidak ada error yang muncul. Perbaikannya membungkus range dengan kurung sebelum metodenya dipanggil.

groovy Contoh: range mundur tanpa kurung
// tanpa kurung: closure menempel pada angka 1, bukan pada range
5..1.each { hari ->
    println "Hari ke-$hari"
}

// perbaikan: bungkus range mundur dengan kurung
(5..1).each { hari ->
    println "Mundur ke-$hari"
}
text Keluaran
Hari ke-1
Mundur ke-5
Mundur ke-4
Mundur ke-3
Mundur ke-2
Mundur ke-1

Kesalahan kedua adalah memakai break atau continue di dalam closure each. Keduanya merupakan pernyataan perulangan, sedangkan closure bukan perulangan bagi kompilator, sehingga kode gagal kompilasi dengan pesan bahwa break hanya boleh berada di dalam perulangan atau switch. Gunakan for dengan in ketika perulangan perlu berhenti lebih awal.

Kesalahan ketiga adalah memberi langkah desimal pada range bilangan bulat, misalnya (1..5).step(0.5). Metode step milik range hanya memiliki tanda tangan step(int), sehingga argumen desimal memicu MissingMethodException. Untuk langkah pecahan, metode step milik Number dapat dipakai, seperti 1.step(3, 0.5) yang menaik dari nilai awal hingga batas atas.

Ringkasan#

  • Range 1..5 inklusif, 1..<5 eksklusif, 5..1 mundur, dan 'a'..'e' berbasis karakter, semuanya objek dengan from, to, size, contains, serta step yang hanya menerima langkah bilangan bulat.
  • Subscript list dengan range menghasilkan potongan, mendukung indeks negatif dan range mundur, dan melempar IndexOutOfBoundsException ketika melampaui batas list.
  • Perulangan memakai each, eachWithIndex, times, upto, downto, dan step berbasis closure, sedangkan for dengan in menjadi tempat break dan continue yang tidak berlaku di dalam closure.
  • Percabangan if/else, ternary, dan Elvis menangani keputusan, sedangkan switch berperan sebagai pernyataan maupun ekspresi dengan case berbentuk nilai, range, koleksi, kelas, dan regex, dan tanpa kecocokan menghasilkan null.

Referensi#

  • Operators — Apache Groovy — dokumentasi resmi operator Groovy yang memuat operator range .. dan ..< beserta perilaku isCase pada pencocokan.
  • Semantics — Apache Groovy — dokumentasi resmi semantik Groovy yang mencakup percabangan if/else, ternary, Elvis, switch, dan perulangan for.