Tugas berkas yang berulang — memindahkan hasil unduhan ke folder per kategori, mengarsipkan laporan lama, memberi nama ulang ratusan foto — adalah pekerjaan yang tidak layak dikerjakan tangan. Chapter sebelumnya memproses isi satu berkas secara bertahap dengan checkpoint; sekarang kita naik satu tingkat: menyapu pohon direktori, mencocokkan nama berkas dengan pola, lalu memindahkan berkas yang cocok secara otomatis. Java menyediakan dua bahan utama untuk pekerjaan ini: PathMatcher dengan sintaks glob yang akrab dari dunia shell, dan Files.walk yang menelusuri direktori berjenjang tanpa rekursi manual. Dua contoh pada chapter ini membentuk satu alat rapikan-unduhan yang utuh — dari pencocokan pola sampai pemindahan — dan keduanya bisa dijalankan ulang dengan hasil yang sama.
Pola Glob dengan PathMatcher #
Setiap otomasi berkas dimulai dari satu pertanyaan: berkas mana yang perlu disentuh? PathMatcher menjawabnya dengan pola glob — sintaks yang sama dengan *.txt di shell. Objek pencocok dibuat lewat FileSystems.getDefault().getPathMatcher("glob:..."): prefiks glob: memilih mesin pencocok glob, sedangkan prefiks regex: memakai mesin java.util.regex penuh. Untuk nama berkas, glob jauh lebih aman daripada regex karena karakter titik berarti titik sungguhan, bukan operator pola. Tabel berikut merangkum sintaks yang paling sering dipakai:
| Pola glob | Makna |
|---|---|
*.txt | nama berakhir .txt pada satu direktori |
**/*.java | berkas .java pada kedalaman berapa pun |
*.{jpg,png,gif} | beberapa ekstensi sekaligus |
? | tepat satu karakter apa pun |
[abc], [a-z] | satu karakter dari himpunan atau rentang |
Program berikut menguji tiga pola terhadap lima nama berkas:
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
public class PolaBerkas {
public static void main(String[] args) {
PathMatcher teks = FileSystems.getDefault().getPathMatcher("glob:*.txt");
PathMatcher gambar = FileSystems.getDefault().getPathMatcher("glob:*.{jpg,png,gif}");
PathMatcher arsip = FileSystems.getDefault().getPathMatcher("glob:*202?*.zip");
String[] nama = {
"laporan.txt", "foto-budi.jpg", "icon.png",
"cadangan-2025.zip", "data.csv"
};
for (String n : nama) {
Path p = Path.of(n);
System.out.printf("%-20s teks=%-5b gambar=%-5b arsip=%b%n",
n, teks.matches(p), gambar.matches(p), arsip.matches(p));
}
}
}Output yang Dihasilkan:
laporan.txt teks=true gambar=false arsip=false
foto-budi.jpg teks=false gambar=true arsip=false
icon.png teks=false gambar=true arsip=false
cadangan-2025.zip teks=false gambar=false arsip=true
data.csv teks=false gambar=false arsip=falseMari kita telaah baris-baris kuncinya:
- Baris
getPathMatcher("glob:*.txt"): prefiksglob:wajib ada; tanpa prefiks,getPathMatchermelemparIllegalArgumentExceptionkarena string pola harus menyatakan mesin pencocoknya. - Baris
glob:*.{jpg,png,gif}: kurung kurawal mendefinisikan kelompok alternatif — satu pola menangkap tiga ekstensi sekaligus. - Baris
glob:*202?*.zip": tanda tanya menggantikan tepat satu karakter, sehinggacadangan-2025.zipcocok sementaracadangan-2025x.ziptidak. - Baris
teks.matches(p): pencocokan berlaku pada keseluruhanPath— pola*.txtcocok untuk path satu nama; untuk path absolut, polanya perlu diawali**/.
Menyapu Pohon Direktori dengan Files.walk #
Pola sudah ada; sasaran berikutnya adalah pohon direktori. Files.walk(akar) mengembalikan Stream<Path> yang malas (lazy) — direktori dibaca bertahap saat stream dikonsumsi. Konsekuensinya penting: pohon tidak boleh diubah selama stream masih terbuka, jadi pola yang benar dua tahap — kumpulkan dulu, ubah kemudian: seluruh hasil dikumpulkan ke List, stream ditutup, baru pemindahan berjalan. Program berikut merapikan folder unduhan/ dengan peta ekstensi ke kategori:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
public class RapihkanUnduhan {
// Peta ekstensi -> kategori folder tujuan.
private static final Map<String, String> KATEGORI = Map.of(
"pdf", "dokumen", "txt", "dokumen", "md", "dokumen",
"jpg", "gambar", "png", "gambar",
"json", "data", "csv", "data",
"py", "skrip");
public static void main(String[] args) throws IOException {
Path unduhan = Path.of("unduhan");
Files.createDirectories(unduhan);
// Pohon contoh dibuat oleh program agar contoh ini mandiri.
for (String n : new String[]{"laporan.pdf", "catatan.txt", "tulisan.md",
"foto-budi.jpg", "icon.png", "penjualan.json", "biaya.csv", "alat.py"}) {
Files.writeString(unduhan.resolve(n), "contoh", StandardCharsets.UTF_8);
}
System.out.println("== sebelum ==");
tampilkanPohon(unduhan);
// Kumpulkan dulu seluruh berkas (stream ditutup) baru dipindahkan,
// agar pohon tidak berubah di tengah penelusuran Files.walk.
List<Path> daftar;
try (Stream<Path> berkas = Files.walk(unduhan)) {
daftar = berkas.filter(Files::isRegularFile).sorted().toList();
}
System.out.println("== pindahan ==");
for (Path p : daftar) {
String kategori = KATEGORI.get(ekstensi(p));
if (kategori != null) {
Path tujuanDir = unduhan.resolve(kategori);
Files.createDirectories(tujuanDir);
Files.move(p, tujuanDir.resolve(p.getFileName()));
System.out.printf("%-16s -> %s/%n", p.getFileName(), kategori);
}
}
System.out.println("== sesudah ==");
tampilkanPohon(unduhan);
}
private static String ekstensi(Path p) {
String nama = p.getFileName().toString();
int titik = nama.lastIndexOf('.');
return titik < 0 ? "" : nama.substring(titik + 1).toLowerCase();
}
private static void tampilkanPohon(Path akar) throws IOException {
try (Stream<Path> semua = Files.walk(akar)) {
semua.sorted().forEach(p -> {
int kedalaman = p.equals(akar) ? 0
: akar.relativize(p).getNameCount();
System.out.println(" ".repeat(kedalaman)
+ (kedalaman == 0 ? "unduhan/" : p.getFileName()));
});
}
System.out.println();
}
}Output yang Dihasilkan:
== sebelum ==
unduhan/
alat.py
biaya.csv
catatan.txt
foto-budi.jpg
icon.png
laporan.pdf
penjualan.json
tulisan.md
== pindahan ==
alat.py -> skrip/
biaya.csv -> data/
catatan.txt -> dokumen/
foto-budi.jpg -> gambar/
icon.png -> gambar/
laporan.pdf -> dokumen/
penjualan.json -> data/
tulisan.md -> dokumen/
== sesudah ==
unduhan/
data
biaya.csv
penjualan.json
dokumen
catatan.txt
laporan.pdf
tulisan.md
gambar
foto-budi.jpg
icon.png
skrip
alat.pyMari kita telaah bagian-bagian pentingnya:
- Baris
Files.createDirectories(unduhan): setaramkdir -p— membuat folder beserta parent yang belum ada, dan tidak protes bila folder sudah ada;Files.createDirectoryyang ketat hanya untuk satu level. - Blok
try (Stream<Path> berkas = Files.walk(unduhan)): seluruh berkas dikumpulkan kedaftardan diurutkan (sorted()) sebelum stream ditutup — urutan pemrosesan menjadi deterministik, dan pemindahan berjalan setelah penelusuran tuntas. - Baris
berkas.filter(Files::isRegularFile):Files.walkjuga memasukkan direktori ke dalam stream; filter memastikan hanya berkas yang dipindahkan, bukan foldernya. - Baris
Files.move(p, tujuanDir.resolve(p.getFileName())): pada sistem berkas yang sama,moveadalah operasi rename yang hampir seketika — tidak menyalin satu byte pun; parameter keduanya adalah path tujuan lengkap, bukan folder tujuan. - Baris
" ".repeat(kedalaman): indentasi dua spasi per level menghasilkan tampilan pohon yang mudah dibaca — dan karena hasilFiles.walkdiurutkan dulu, keluarannya selalu sama di setiap eksekusi.
Dari Perintah Shell ke API Java #
Sebagian besar otomasi berkas terjemahan langsung dari kebiasaan shell — dan tiap padanannya di NIO membawa keunggulan tambahan:
| Tugas | Perintah shell | API Java |
|---|---|---|
| Pindahkan / ganti nama | mv | Files.move(asal, tujuan) |
| Buat folder berjenjang | mkdir -p | Files.createDirectories(path) |
| Hapus | rm | Files.delete(path) |
| Telusuri berjenjang | find | Files.walk(akar) |
| Cocokkan pola | glob shell | PathMatcher |
Versi Java berjalan lintas platform tanpa utilitas eksternal, menolak nama berkas tidak sah dengan exception jelas, dan — sebagaimana dibahas pada chapter Keamanan Interoperability — tidak membuka celah injeksi perintah karena tidak ada shell yang dieksekusi.
Alat kita kini mampu merapikan pohon direktori setiap kali dipanggil. Pertanyaan berikutnya adalah waktu panggilnya: tugas otomasi jarang sekali — biasanya tiap malam atau tiap pekan. Chapter berikutnya mengatur jadwal itu dari dalam JVM lewat ScheduledExecutorService, serta membandingkannya dengan penjadwal tingkat sistem operasi.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PolaBerkas.java
Unduh berkasimport java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
public class PolaBerkas {
public static void main(String[] args) {
PathMatcher teks = FileSystems.getDefault().getPathMatcher("glob:*.txt");
PathMatcher gambar = FileSystems.getDefault().getPathMatcher("glob:*.{jpg,png,gif}");
PathMatcher arsip = FileSystems.getDefault().getPathMatcher("glob:*202?*.zip");
String[] nama = {
"laporan.txt", "foto-budi.jpg", "icon.png",
"cadangan-2025.zip", "data.csv"
};
for (String n : nama) {
Path p = Path.of(n);
System.out.printf("%-20s teks=%-5b gambar=%-5b arsip=%b%n",
n, teks.matches(p), gambar.matches(p), arsip.matches(p));
}
}
}
RapihkanUnduhan.java
Unduh berkasimport java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
public class RapihkanUnduhan {
// Peta ekstensi -> kategori folder tujuan.
private static final Map<String, String> KATEGORI = Map.of(
"pdf", "dokumen", "txt", "dokumen", "md", "dokumen",
"jpg", "gambar", "png", "gambar",
"json", "data", "csv", "data",
"py", "skrip");
public static void main(String[] args) throws IOException {
Path unduhan = Path.of("unduhan");
Files.createDirectories(unduhan);
// Pohon contoh dibuat oleh program agar contoh ini mandiri.
for (String n : new String[]{"laporan.pdf", "catatan.txt", "tulisan.md",
"foto-budi.jpg", "icon.png", "penjualan.json", "biaya.csv", "alat.py"}) {
Files.writeString(unduhan.resolve(n), "contoh", StandardCharsets.UTF_8);
}
System.out.println("== sebelum ==");
tampilkanPohon(unduhan);
// Kumpulkan dulu seluruh berkas (stream ditutup) baru dipindahkan,
// agar pohon tidak berubah di tengah penelusuran Files.walk.
List<Path> daftar;
try (Stream<Path> berkas = Files.walk(unduhan)) {
daftar = berkas.filter(Files::isRegularFile).sorted().toList();
}
System.out.println("== pindahan ==");
for (Path p : daftar) {
String kategori = KATEGORI.get(ekstensi(p));
if (kategori != null) {
Path tujuanDir = unduhan.resolve(kategori);
Files.createDirectories(tujuanDir);
Files.move(p, tujuanDir.resolve(p.getFileName()));
System.out.printf("%-16s -> %s/%n", p.getFileName(), kategori);
}
}
System.out.println("== sesudah ==");
tampilkanPohon(unduhan);
}
private static String ekstensi(Path p) {
String nama = p.getFileName().toString();
int titik = nama.lastIndexOf('.');
return titik < 0 ? "" : nama.substring(titik + 1).toLowerCase();
}
private static void tampilkanPohon(Path akar) throws IOException {
try (Stream<Path> semua = Files.walk(akar)) {
semua.sorted().forEach(p -> {
int kedalaman = p.equals(akar) ? 0
: akar.relativize(p).getNameCount();
System.out.println(" ".repeat(kedalaman)
+ (kedalaman == 0 ? "unduhan/" : p.getFileName()));
});
}
System.out.println();
}
}