Metode ekstensi GDK (Groovy Development Kit) menambahkan kemampuan praktis pada kelas java.io.File, sehingga pembacaan, penulisan, dan penjelajahan berkas selesai dalam beberapa baris tanpa kelas bantu yang ditulis sendiri. Topik ini memperluas pengenalan singkat yang dimulai pada topik metode ekstensi GDK. Di dalamnya, isi berkas dibaca dengan text, readLines, eachLine, withReader, dan bytes, ditulis dengan write, append, dan withWriter, resource dikelola otomatis, folder dijelajahi, serta utilitas kecil seperti exists, mkdirs, dan length dipakai. Setelah membacanya, pekerjaan berkas pada skrip otomasi sehari-hari dapat ditangani lengkap karena penutupan resource dan struktur folder diurus oleh metode yang dipanggil, sedangkan kebutuhan yang lebih besar ditangani NIO milik JDK.
Membaca isi berkas#
Pembacaan berkas teks memiliki empat jalur GDK dengan tingkat kontrol yang bertingkat, yaitu properti text untuk seluruh isi sekaligus, readLines untuk daftar baris, eachLine untuk pemrosesan per baris, dan withReader untuk pembacaan yang diatur di dalam closure. Properti text paling ringkas tetapi menampung seluruh isi dalam memori, sehingga cocok untuk berkas kecil seperti arsip catatan. Metode readLines memecah isi menjadi List<String> yang setiap elemennya dapat diindeks. Metode eachLine menyalurkan baris demi baris ke closure dan mengirim nomor baris mulai dari 1 sebagai argumen kedua, sehingga perhitungan nomor tidak perlu ditulis sendiri. Skrip berikut menyiapkan arsip catatan contoh, lalu membacanya dengan tiga jalur pertama.
// baca-catatan.groovy: membaca isi berkas dengan metode ekstensi GDK
def arsip = new File('arsip-catatan.txt')
// berkas contoh disiapkan dengan metode write()
arsip.write('21 Januari: merapikan arsip\n' +
'22 Januari: menandai catatan lama\n' +
'23 Januari: menyusun indeks\n')
println '--- properti text'
println arsip.text
println '--- readLines'
println "Jumlah baris: ${arsip.readLines().size()}"
println "Baris kedua : ${arsip.readLines()[1]}"
println '--- eachLine dengan nomor baris'
arsip.eachLine { baris, nomor -> println "${nomor}: ${baris}" }--- properti text
21 Januari: merapikan arsip
22 Januari: menandai catatan lama
23 Januari: menyusun indeks
--- readLines
Jumlah baris: 3
Baris kedua : 22 Januari: menandai catatan lama
--- eachLine dengan nomor baris
1: 21 Januari: merapikan arsip
2: 22 Januari: menandai catatan lama
3: 23 Januari: menyusun indeksKeluaran di atas memperlihatkan perbedaan ketiganya secara langsung. Properti text mengembalikan isi persis sebagaimana tersimpan, termasuk baris kosong yang berasal dari tanda baris baru pada ujung berkas. Metode readLines mengabaikan baris kosong penutup dan menghasilkan tepat tiga elemen, sedangkan eachLine melaporkan nomor baris tanpa perhitungan manual. Untuk isi biner, properti bytes membaca berkas sebagai array byte, dan penugasan ke bytes menulis isi tersebut balik ke berkas yang sama. Pembacaan yang lebih terarah memakai withReader, yaitu membuka Reader yang dibaca baris demi baris dengan readLine(), sehingga isi diperoleh secara bertahap dan pembacaan dapat berhenti lebih awal begitu baris yang dicari ditemukan.
def arsip = new File('arsip-catatan.txt')
arsip.write('21 Januari: merapikan arsip\n' +
'22 Januari: menandai catatan lama\n' +
'23 Januari: menyusun indeks\n')
// withReader: Reader dibuka dan ditutup otomatis
arsip.withReader { pembaca ->
println "Baris pertama : ${pembaca.readLine()}"
println "Baris kedua : ${pembaca.readLine()}"
}
// bytes: membaca dan menulis isi biner
def stempel = new File('stempel-arsip.bin')
stempel.bytes = [0x47, 0x44, 0x4B] as byte[]
def bacaan = stempel.bytes
println "Jumlah byte : ${bacaan.length}"
println "Isi teksnya : ${new String(bacaan, 'US-ASCII')}"Baris pertama : 21 Januari: merapikan arsip
Baris kedua : 22 Januari: menandai catatan lama
Jumlah byte : 3
Isi teksnya : GDKFragmen tersebut menggabungkan dua kebutuhan yang berbeda. withReader menyediakan Reader untuk teks yang dibaca bertahap, sedangkan bytes menangani isi biner seperti stempel pengenal dalam bentuk tiga byte yang kebetulan juga terbaca sebagai teks GDK. Keduanya menutup sumber datanya sendiri sesuai pola yang dibedah pada bagian berikutnya.
Menulis dan menambah isi berkas#
Penulisan isi berkas memakai pasangan metode yang setara dengan pembacaannya, yaitu write untuk menimpa isi dari awal, append untuk menambah di akhir, dan withWriter untuk menulis banyak baris melalui satu penulis. Metode write membuat berkas baru bila belum ada dan mengganti seluruh isinya bila sudah ada, sedangkan append menambahkan teks tanpa menyentuh isi lama. Sebagai jalan pintas, penugasan ke properti text adalah bentuk singkat dari write. Skrip berikut merekam catatan suhu sensor dengan ketiga metode itu, lalu memanggil write untuk kedua kalinya agar efek penimpaan terlihat jelas.
// tulis-catatan.groovy: menimpa, menambah, dan menulis lewat penulis
def catatan = new File('catatan-sensor.txt')
catatan.write('07.00, suhu 24\n')
catatan.append('13.00, suhu 27\n')
catatan.append('17.00, suhu 25\n')
catatan.eachLine { println it }
// write() kedua menghapus seluruh isi lama tanpa peringatan
catatan.write('07.00, suhu 24\n')
println '--- sesudah write() ulang'
catatan.eachLine { println it }
// withWriter: banyak baris ditulis melalui satu penulis
catatan.withWriter { penulis ->
penulis.writeLine('07.00, suhu 24')
penulis.writeLine('13.00, suhu 27')
penulis.writeLine('17.00, suhu 25')
}
println '--- hasil withWriter'
catatan.eachLine { println it }07.00, suhu 24
13.00, suhu 27
17.00, suhu 25
--- sesudah write() ulang
07.00, suhu 24
--- hasil withWriter
07.00, suhu 24
13.00, suhu 27
17.00, suhu 25Tiga bagian keluaran menelusuri siklusnya secara berurutan. Bagian pertama berisi tiga baris, yaitu hasil write awal ditambah dua panggilan append. Bagian kedua menyisakan satu baris saja karena write kedua menghapus seluruh isi lama tanpa konfirmasi. Bagian ketiga menulis ulang tiga baris sekaligus melalui withWriter, yaitu metode yang menerima closure dengan Writer sebagai parameternya. Metode writeLine milik penulis itulah yang menambahkan tanda baris baru pada setiap panggilan. Untuk kebutuhan serupa dalam mode tambah, GDK menyediakan withWriterAppend yang bekerja seperti withWriter tetapi mempertahankan isi lama.
Pengelolaan resource otomatis#
Keluarga metode with* mengurus resource dengan pola yang seragam, yaitu sumber daya seperti berkas terbuka yang wajib ditutup setelah selesai dipakai. Metode membuka resource, menjalankan closure yang menerimanya, lalu menutup resource itu kembali, dan penutupan tetap terjadi sekalipun closure melempar exception. Pola ini menggantikan bentuk manual Java yang menaruh close di dalam blok finally. Metode pada berkas yang mengikuti pola ini antara lain withReader, withWriter, withWriterAppend, withInputStream, dan withOutputStream, sedangkan withCloseable berlaku untuk setiap objek Closeable. Karena penutupan pada berkas sungguhan sulit diamati, skrip berikut membuktikannya dengan kelas Writer uji yang penghitung statisnya bertambah setiap kali close dipanggil.
// Kelas Writer uji: setiap close() menambah penghitung statis
class PenulisUji extends Writer {
static int jumlahTutup = 0
void write(char[] cbuf, int off, int len) { }
void flush() { }
void close() { jumlahTutup += 1 }
}
// withCloseable menutup resource meski closure melempar exception
try {
new PenulisUji().withCloseable { penulis ->
penulis.write('data')
throw new RuntimeException('simulasi kegagalan')
}
} catch (RuntimeException e) {
println "dengan withCloseable : close terpanggil ${PenulisUji.jumlahTutup} kali"
}
// penutupan manual tanpa try/finally: exception memotong jalur ke close()
def tugasGagal = { throw new RuntimeException('simulasi kegagalan') }
try {
PenulisUji.jumlahTutup = 0
def penulis = new PenulisUji()
penulis.write('data')
tugasGagal()
penulis.close() // baris ini tidak pernah tercapai
} catch (RuntimeException e) {
println "tanpa try/finally : close terpanggil ${PenulisUji.jumlahTutup} kali"
}dengan withCloseable : close terpanggil 1 kali
tanpa try/finally : close terpanggil 0 kaliKeluaran membuktikan dua jalur sekaligus. Pemanggilan withCloseable yang gagal di tengah closure tetap meninggalkan penghitung bernilai satu, yang berarti close tetap dijalankan ketika exception menyebar keluar. Pada blok kedua, penutupan yang ditulis manual tanpa try dan finally terpotong oleh exception yang sama, sehingga penghitung tetap nol dan resource tertinggal terbuka. Penghitung dipakai alih-alih mencetak sesuatu dari close, karena penutupan pada Writer uji tidak menghasilkan keluaran yang dapat diamati. Mekanisme yang sama berlaku pada withReader dan withWriter milik File, sehingga pilihan with* bukan sekadar pemendekan baris, melainkan jaminan penutupan yang mudah luput bila ditulis manual.
Menjelajah isi folder#
Penjelajahan folder tersedia dalam tiga metode dengan kedalaman yang berbeda, yaitu eachFile untuk satu level, eachFileRecurse untuk seluruh turunan, dan traverse untuk kunjungan yang dapat diatur lewat opsi. Ketiganya menerima closure yang dipanggil untuk setiap objek File yang ditemukan. Metode traverse menerima opsi berbentuk map, antara lain type dengan nilai FileType.FILES untuk berkas saja, filter berupa closure pemilih, dan sort berupa closure pembanding untuk mengurutkan kunjungan. Kelas FileType berada di paket groovy.io, sehingga skrip perlu mengimpornya terlebih dahulu. Skrip berikut menyiapkan folder arsip contoh, lalu menjelajahnya dengan ketiga metode tersebut.
// jelajah-direktori.groovy: menjelajah folder dengan tiga metode GDK
import groovy.io.FileType
// siapkan struktur arsip contoh. deleteDir menghapus sisa uji sebelumnya
def arsip = new File('data/arsip')
arsip.deleteDir()
arsip.mkdirs()
new File(arsip, 'catatan-a.txt').write('isi A')
new File(arsip, 'catatan-b.txt').write('isi B')
def lampiran = new File(arsip, 'lampiran')
lampiran.mkdirs()
new File(lampiran, 'lampiran-1.txt').write('isi lampiran 1')
new File(lampiran, 'lampiran-2.txt').write('isi lampiran 2')
println '--- eachFile: satu level'
def levelPertama = []
arsip.eachFile { f -> levelPertama << "${f.name} (${f.isDirectory() ? 'folder' : 'berkas'})" }
levelPertama.sort().each { println it }
println '--- eachFileRecurse: seluruh berkas turunan'
def seluruhnya = []
arsip.eachFileRecurse(FileType.FILES) { f -> seluruhnya << f.path }
seluruhnya.sort().each { println it }
println '--- traverse: berkas catatan saja, terurut'
arsip.traverse(type: FileType.FILES, filter: { it.name.startsWith('catatan') },
sort: { a, b -> a.name <=> b.name }) { f -> println f.path }--- eachFile: satu level
catatan-a.txt (berkas)
catatan-b.txt (berkas)
lampiran (folder)
--- eachFileRecurse: seluruh berkas turunan
data/arsip/catatan-a.txt
data/arsip/catatan-b.txt
data/arsip/lampiran/lampiran-1.txt
data/arsip/lampiran/lampiran-2.txt
--- traverse: berkas catatan saja, terurut
data/arsip/catatan-a.txt
data/arsip/catatan-b.txtBagian pertama hanya menyentuh level teratas, sehingga folder lampiran muncul sebagai satu entri berlabel folder. Bagian kedua memakai FileType.FILES sehingga hanya berkas yang dilaporkan, termasuk dua berkas yang berada di dalam lampiran. Bagian ketiga mempersempit hasil lewat filter dan mengurutkan kunjungan dengan closure pembanding sort yang memakai operator <=> sebagaimana lazim pada pengurutan koleksi. Untuk kebutuhan yang lebih besar, misalnya pohon folder yang dalam atau aturan kunjungan yang kompleks, JDK menyediakan pasangan modernnya lewat NIO, yaitu Path dan Files.walkFileTree dalam paket java.nio.file. GDK tetap menjadi pilihan ringkas untuk skrip otomasi sehari-hari.
Utilitas kecil pada File#
Di luar isi berkasnya, File membawa utilitas kecil untuk memeriksa dan merapikan keberadaan berkas serta folder. Metode exists menguji keberadaan, isFile dan isDirectory membedakan jenisnya, length mengukur ukuran dalam byte, mkdirs membuat folder lengkap dengan seluruh induknya, delete menghapus berkas, dan deleteDir menyapu folder beserta isinya. Nilai kembalian metode-metode ini berupa boolean, sehingga tahap demi tahapnya mudah ditelusuri lewat keluaran. Pemeriksaan exists sebelum menjalankan aksi juga menjadi pembuka yang wajar pada skrip yang dijalankan ulang. Skrip berikut mengikuti kehidupan singkat sebuah daftar belanja dan satu folder arsip tahunan.
def belanja = new File('daftar-belanja.txt')
belanja.delete() // bersihkan sisa uji sebelumnya
println "Sebelum dibuat : ada=${belanja.exists()}"
belanja.write('beras\nminyak goreng\ntelur\n')
println "Sesudah dibuat : ada=${belanja.exists()}, berkas=${belanja.isFile()}"
println "Ukuran : ${belanja.length()} byte"
// folder dibuat berjenjang sampai induknya sekalian
def arsipBaru = new File('data/arsip-2026')
arsipBaru.deleteDir()
println "Folder dibuat : ${arsipBaru.mkdirs()}"
println "Folder ada : ${arsipBaru.isDirectory()}"
println "Hapus berkas : ${belanja.delete()}"
println "Sesudah dihapus: ada=${belanja.exists()}"Sebelum dibuat : ada=false
Sesudah dibuat : ada=true, berkas=true
Ukuran : 26 byte
Folder dibuat : true
Folder ada : true
Hapus berkas : true
Sesudah dihapus: ada=falseKeluaran menelusuri setiap tahap secara berurutan, dari ketiadaan berkas, keberadaannya bersama ukuran 26 byte, pembuatan folder, hingga penghapusannya kembali. Perhatikan bahwa length dipanggil dengan tanda kurung karena File tidak memiliki properti length dan penulisan tanpa tanda kurung melempar MissingPropertyException. Nilai true yang dikembalikan mkdirs berarti pembuatan berhasil pada pemanggilan itu, sedangkan pemanggilan kedua pada folder yang sudah ada mengembalikan false tanpa melempar error. Untuk folder yang masih berisi isi, delete mengembalikan false, dan penghapusannya memakai deleteDir yang menyapu seluruh turunannya.
Kesalahan umum#
Kesalahan pertama adalah membaca berkas yang belum ada. Pembacaan apa pun pada File yang tidak ada melempar FileNotFoundException, dan pesan lengkapnya menyebut alamat berkas yang gagal dibaca. Ketiadaan berkas kadang memang kondisi yang wajar, misalnya pada skrip yang dijalankan pertama kali, sehingga penanganannya dipilih sesuai situasi.
try {
new File('catatan-tidak-ada.txt').text
} catch (FileNotFoundException e) {
println "Gagal: ${e.getClass().simpleName}"
}Gagal: FileNotFoundExceptionPraktik yang sehat adalah memeriksa exists terlebih dahulu bila ketiadaan berkas adalah kondisi yang diharapkan, dan menangkap exception bila ketiadaannya berarti gangguan yang perlu dilaporkan. Penangkapan pada contoh menyasar FileNotFoundException secara spesifik, sehingga error lain tetap menyebar dan tidak tersamarkan. Perlu diingat, exception yang sama juga muncul bila folder induk berkas belum ada, sehingga konteks pemanggilan yang menentukan penanganannya.
Kesalahan kedua adalah menulis ke alamat yang folder induknya belum ada. Metode write membuat berkasnya, tetapi tidak membuat foldernya, sehingga penulisan ke folder-tidak-ada/laporan.txt melempar FileNotFoundException yang sama dengan kasus pembacaan.
new File('folder-tidak-ada').deleteDir() // bersihkan sisa uji sebelumnya
def laporan = new File('folder-tidak-ada/laporan.txt')
try {
laporan.write('laporan harian')
} catch (FileNotFoundException e) {
println "Gagal: ${e.getClass().simpleName}"
}
laporan.parentFile.mkdirs()
laporan.write('laporan harian')
println "Sesudah folder dibuat: ada=${laporan.exists()}"Gagal: FileNotFoundException
Sesudah folder dibuat: ada=truePerbaikannya memakai properti parentFile, yaitu objek File yang menunjuk folder induk berkas tersebut. Memanggil parentFile.mkdirs() lebih dahulu membuat seluruh induknya, lalu penulisan diulang dan berhasil. Pola ini lazim dipakai pada skrip yang menyusun struktur keluarannya sendiri, misalnya membuat folder per bulan sebelum menulis catatan harian.
Ringkasan#
- Properti
text, metodereadLines,eachLine,withReader, danbytesmenutup kebutuhan pembacaan teks maupun biner, sedangkanwrite,append,withWriter, danwithWriterAppendmenutup kebutuhan penulisannya. - Metode
with*membuka resource, menjalankan closure, dan menutupnya kembali meski exception terjadi, sehingga bloktrydanfinallymanual tidak perlu ditulis. eachFile,eachFileRecurse, dantraversemenjelajah folder dengan kedalaman serta penyaringan yang berbeda, dan urutan kunjungannya mengikuti sistem berkas sehingga perlu diurutkan bila keluaran harus stabil.- Utilitas
exists,isFile,length,mkdirs,delete, dandeleteDirmengelola keberadaan berkas dan folder, sedangkan penjelajahan berskala besar diserahkan kejava.nio.file.
Referensi#
- Working with IO — Apache Groovy — bagian resmi dokumentasi Groovy 5.1 tentang pembacaan, penulisan, dan penjelajahan berkas dengan metode ekstensi GDK.
- ResourceGroovyMethods — Apache Groovy — javadoc metode ekstensi pada berkas seperti eachLine, eachFile, eachFileRecurse, traverse, deleteDir, dan setText.
- IOGroovyMethods — Apache Groovy — javadoc metode pada Reader dan Writer, termasuk readLine, writeLine, withWriter, dan withCloseable.
- File (Java SE 21 & JDK 21) — Oracle — dokumentasi kelas JDK yang menjadi penerima metode ekstensi GDK, termasuk exists, length, dan mkdirs.