Tampilan
Kenapa penting
Hampir setiap backend menyentuh file: membaca konfigurasi, menerima upload bukti transfer, mengimpor CSV produk dari pemasok, menulis laporan harian, atau memindahkan file yang sudah diproses ke folder arsip. Kesalahan kecil di sini mahal: encoding salah membuat nama pelanggan jadi karakter aneh, file besar yang dimuat sekaligus membuat server kehabisan memori, stream yang tidak ditutup menghabiskan file handle, dan nama file upload yang dipercaya begitu saja membuka celah path traversal. Java modern menyediakan paket java.nio.file — Path untuk lokasi dan Files untuk operasi — yang jauh lebih jelas dan aman daripada class java.io.File lama.
Konsep inti
Path: lokasi, bukan isi file
Path mewakili alamat file atau folder. Membuat Path tidak menyentuh disk — file-nya boleh belum ada. Buat dengan Path.of(...) (sejak Java 11); Paths.get(...) adalah cara lama yang setara dan masih sering kamu temui di kode warisan.
java
import java.nio.file.Path;
public class PathDemo {
public static void main(String[] args) {
Path base = Path.of("data", "uploads"); // relatif terhadap working directory
Path invoice = base.resolve("2026/10/INV-001.pdf");
System.out.println(invoice);
System.out.println(invoice.getFileName());
System.out.println(invoice.getParent());
System.out.println(invoice.isAbsolute());
System.out.println(base.relativize(invoice));
Path messy = Path.of("data/uploads/./2026/../2026/10/INV-001.pdf");
System.out.println(messy.normalize());
System.out.println(messy.normalize().equals(invoice));
}
}Output (di Linux/macOS; Windows memakai \ sebagai pemisah):
text
data/uploads/2026/10/INV-001.pdf
INV-001.pdf
data/uploads/2026/10
false
2026/10/INV-001.pdf
data/uploads/2026/10/INV-001.pdf
true| Method | Fungsi |
|---|---|
resolve(x) | gabungkan path ini dengan x (bila x absolut, hasilnya x!) |
normalize() | buang . dan gabungkan nama/.. — murni olah teks |
relativize(p) | path relatif dari path ini menuju p |
toAbsolutePath() | jadikan absolut berdasarkan working directory |
getFileName(), getParent() | bagian terakhir, dan folder induknya |
Path relatif diselesaikan terhadap working directory proses Java — folder tempat aplikasi dijalankan, bukan folder file .java. Ini sumber klasik bug "jalan di IDE, gagal di server". Di aplikasi nyata, folder dasar sebaiknya datang dari konfigurasi dan dijadikan absolut sekali di awal.
Membaca & menulis file teks
Class utilitas Files berisi method statis untuk hampir semua operasi. Untuk teks, selalu sebutkan charset — StandardCharsets.UTF_8 — agar maksud kodenya jelas dan tidak bergantung pada platform.
java
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.stream.Stream;
public class ReadWriteDemo {
public static void main(String[] args) throws IOException {
Path dir = Files.createTempDirectory("jalurjava-");
Path notes = dir.resolve("catatan.txt");
// 1. Tulis & baca seluruh isi sebagai String
Files.writeString(notes, "Kopi Gayo — stok 12\n", StandardCharsets.UTF_8);
Files.writeString(notes, "Teh Melati — stok 0\n", StandardCharsets.UTF_8, StandardOpenOption.APPEND);
System.out.print(Files.readString(notes, StandardCharsets.UTF_8));
// 2. Baca per baris ke List (seluruh file masuk memori)
List<String> lines = Files.readAllLines(notes, StandardCharsets.UTF_8);
System.out.println(lines.size() + " baris, pertama: " + lines.getFirst());
// 3. Stream baris secara lazy — WAJIB ditutup
try (Stream<String> stream = Files.lines(notes, StandardCharsets.UTF_8)) {
long empty = stream.filter(line -> line.endsWith("stok 0")).count();
System.out.println("Stok habis: " + empty);
}
// 4. Buffered writer & reader untuk menulis/membaca bertahap
Path report = dir.resolve("laporan.csv");
try (BufferedWriter writer = Files.newBufferedWriter(report, StandardCharsets.UTF_8)) {
writer.write("sku,qty");
writer.newLine();
writer.write("KOPI-250,12");
writer.newLine();
}
try (BufferedReader reader = Files.newBufferedReader(report, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("> " + line);
}
}
System.out.println(Files.size(report) + " byte");
}
}Output:
text
Kopi Gayo — stok 12
Teh Melati — stok 0
2 baris, pertama: Kopi Gayo — stok 12
Stok habis: 1
> sku,qty
> KOPI-250,12
20 bytewriteString/newBufferedWritertanpa opsi tambahan akan membuat file bila belum ada dan menimpa isinya bila sudah ada. TambahkanStandardOpenOption.APPENDuntuk menambah di akhir.readString/readAllLinesmemuat seluruh file ke memori — praktis untuk file kecil (konfigurasi, template).Files.linesmengembalikanStream<String>yang membaca baris secara lazy. Stream ini memegang file handle yang terbuka, jadi selalu bungkus dengan try-with-resources — sama sepertiBufferedReader.
Charset: teks hanyalah byte
File menyimpan byte, bukan huruf. Charset adalah aturan menerjemahkan byte ke karakter. Sejak Java 18 (JEP 400) charset default adalah UTF-8, dan method Files tanpa parameter charset juga selalu memakai UTF-8. Masalah muncul saat file datang dari sistem lain:
java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class CharsetDemo {
public static void main(String[] args) throws IOException {
Path file = Files.createTempFile("pelanggan-", ".csv");
Files.writeString(file, "Kafé Ñusa,Bandung", StandardCharsets.ISO_8859_1); // file dari sistem lama
System.out.println(Files.size(file) + " byte");
System.out.println(Files.readString(file, StandardCharsets.ISO_8859_1));
try {
Files.readString(file, StandardCharsets.UTF_8);
} catch (IOException e) {
System.out.println(e);
} finally {
Files.delete(file);
}
}
}Output:
text
17 byte
Kafé Ñusa,Bandung
java.nio.charset.MalformedInputException: Input length = 3readString menolak byte yang bukan UTF-8 valid dengan MalformedInputException (subclass IOException) — lebih baik daripada diam-diam menghasilkan teks rusak. Solusinya: tanyakan charset asli file, baca dengan charset itu, lalu simpan ulang sebagai UTF-8.
Membuat, menyalin, memindah, menghapus, menelusuri
java
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Comparator;
import java.util.stream.Stream;
public class FileOpsDemo {
public static void main(String[] args) throws IOException {
Path root = Files.createTempDirectory("ops-");
Path inbox = Files.createDirectories(root.resolve("inbox/2026/10"));
Path archive = Files.createDirectories(root.resolve("archive"));
Path order = Files.writeString(inbox.resolve("order-1.json"), "{\"id\":1}", StandardCharsets.UTF_8);
Files.copy(order, inbox.resolve("order-1.bak"), StandardCopyOption.REPLACE_EXISTING);
Files.move(order, archive.resolve("order-1.json"), StandardCopyOption.REPLACE_EXISTING);
System.out.println(Files.exists(order) + " " + Files.exists(archive.resolve("order-1.json")));
try (Stream<Path> paths = Files.walk(root)) {
paths.filter(Files::isRegularFile)
.map(p -> root.relativize(p).toString())
.sorted()
.forEach(System.out::println);
}
try {
Files.readString(root.resolve("tidak-ada.txt"));
} catch (NoSuchFileException e) {
System.out.println("File tidak ditemukan: " + root.relativize(Path.of(e.getFile())));
}
System.out.println(Files.deleteIfExists(root.resolve("tidak-ada.txt")));
// Hapus folder beserta isinya: anak dulu, baru induk (urutan terbalik)
try (Stream<Path> paths = Files.walk(root)) {
paths.sorted(Comparator.reverseOrder()).forEach(p -> {
try {
Files.delete(p);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
System.out.println(Files.exists(root));
}
}Output:
text
false true
archive/order-1.json
inbox/2026/10/order-1.bak
File tidak ditemukan: tidak-ada.txt
false
false| Operasi | Catatan |
|---|---|
createDirectories(p) | buat folder beserta induknya; tidak error bila sudah ada |
createDirectory(p) | hanya satu level; error bila sudah ada atau induk belum ada |
copy(a, b, REPLACE_EXISTING) | tanpa opsi itu, gagal dengan FileAlreadyExistsException bila b ada |
move(a, b, ...) | pindah/ganti nama; ATOMIC_MOVE bila pembaca tak boleh melihat file setengah jadi |
delete(p) / deleteIfExists(p) | delete melempar NoSuchFileException bila tidak ada; folder harus kosong |
walk(p) | Stream<Path> seluruh isi folder secara rekursif — wajib ditutup |
createTempFile/createTempDirectory | nama unik di folder temp sistem; tidak terhapus otomatis |
Hampir semua method Files melempar IOException yang checked. Subclass-nya memberi tahu penyebab spesifik: NoSuchFileException, FileAlreadyExistsException, DirectoryNotEmptyException, AccessDeniedException. Tangkap yang spesifik bila kamu punya respons berbeda (misalnya "file tidak ada" → pakai konfigurasi default), sisanya biarkan naik. Di dalam lambda, IOException tidak bisa dilempar langsung, sehingga dibungkus UncheckedIOException seperti contoh di atas.
File besar: streaming vs memuat semua
| Pendekatan | Memori | Cocok untuk |
|---|---|---|
readString, readAllLines | sebesar file | konfigurasi, template, file < beberapa MB |
Files.lines, newBufferedReader | satu baris | log, CSV impor, export besar |
Files.copy(InputStream, Path) | buffer kecil | menyimpan upload tanpa memuatnya ke byte[] |
Aturan praktis: bila ukuran file ditentukan pihak luar (pengguna, pemasok, sistem lain), anggap bisa sangat besar dan proses secara streaming. Kompleksitas waktunya tetap O(n), tetapi memorinya O(1) terhadap jumlah baris.
Contoh di dunia backend
Layanan toko menerima upload CSV laporan penjualan dari mitra, menyimpannya di folder upload, lalu menghitung omzet. Dua hal penting: nama file dari klien tidak dipercaya, dan file disalin lewat file sementara lalu dipindah agar proses lain tidak membaca file setengah jadi.
java
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public class UploadStorage {
private final Path baseDir;
UploadStorage(Path baseDir) throws IOException {
this.baseDir = Files.createDirectories(baseDir).toAbsolutePath().normalize();
}
/** Simpan file upload; nama file dari klien TIDAK dipercaya. */
Path save(String clientFileName, InputStream content) throws IOException {
Path target = baseDir.resolve(clientFileName).normalize();
if (!target.startsWith(baseDir) || target.equals(baseDir)) {
throw new IllegalArgumentException("Nama file tidak valid: " + clientFileName);
}
Files.createDirectories(target.getParent());
// Tulis ke file sementara di folder yang sama, lalu pindahkan → pembaca tidak melihat file setengah jadi
Path temp = Files.createTempFile(baseDir, "upload-", ".part");
try {
Files.copy(content, temp, StandardCopyOption.REPLACE_EXISTING);
return Files.move(temp, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} finally {
Files.deleteIfExists(temp);
}
}
/** Hitung total omzet dari CSV besar baris demi baris — memori tetap kecil berapa pun ukurannya. */
long sumRevenue(Path csv) throws IOException {
long total = 0;
int lineNo = 0;
try (BufferedReader reader = Files.newBufferedReader(csv, StandardCharsets.UTF_8)) {
reader.readLine(); // lewati header
String line;
while ((line = reader.readLine()) != null) {
lineNo++;
if (line.isBlank()) continue;
String[] cols = line.split(",");
try {
total += Long.parseLong(cols[1].trim()) * Integer.parseInt(cols[2].trim());
} catch (RuntimeException e) {
System.out.println("Lewati baris data " + lineNo + ": " + line);
}
}
}
return total;
}
public static void main(String[] args) throws IOException {
Path base = Files.createTempDirectory("store-");
UploadStorage storage = new UploadStorage(base.resolve("uploads"));
String csv = "sku,price,qty\nKOPI-250,85000,2\nTEH-100,40000,x\nGULA-500,30000,3\n";
Path saved = storage.save("laporan/okt.csv",
new ByteArrayInputStream(csv.getBytes(StandardCharsets.UTF_8)));
System.out.println("Tersimpan: " + base.relativize(saved));
System.out.println("Omzet: " + storage.sumRevenue(saved));
try {
storage.save("../../etc/cron.d/jahat", new ByteArrayInputStream(new byte[0]));
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
}Output:
text
Tersimpan: uploads/laporan/okt.csv
Lewati baris data 2: TEH-100,40000,x
Omzet: 260000
Nama file tidak valid: ../../etc/cron.d/jahatCek startsWith(baseDir) setelah normalize() menolak .. maupun nama absolut seperti /etc/passwd (karena resolve dengan path absolut menghasilkan path absolut itu sendiri). Di produksi, langkah yang lebih kuat adalah tidak memakai nama dari klien sama sekali: buat nama acak di server (misalnya UUID), simpan nama asli di database, batasi ukuran dan tipe file, dan simpan upload di luar folder aplikasi. Bila folder upload bisa berisi symlink, bandingkan juga hasil toRealPath(). ATOMIC_MOVE hanya bisa dalam satu file system — itu sebabnya file sementara dibuat di baseDir, bukan di folder temp sistem.
Pertanyaan interview
1. Apa beda Files.readAllLines dan Files.lines?
readAllLines membaca seluruh file dan mengembalikan List<String> — semua baris ada di memori sekaligus. Files.lines mengembalikan Stream<String> yang membaca baris secara lazy, sehingga cocok untuk file besar. Karena stream itu memegang file handle yang terbuka, ia harus ditutup dengan try-with-resources.
2. Apa itu path traversal dan bagaimana mencegahnya di Java?
Penyerang menyisipkan ../ (atau path absolut) di nama file agar server membaca/menulis di luar folder yang diizinkan. Cegah dengan menggabungkan nama ke folder dasar, normalize(), lalu menolak bila hasilnya tidak startsWith(baseDir). Lebih aman lagi: jangan pakai nama dari klien sebagai nama file — buat nama sendiri.
3. Kenapa charset sebaiknya ditulis eksplisit walaupun default Java sudah UTF-8?
Agar maksud kode jelas dan perilakunya tidak bergantung pada versi Java atau setelan platform. Class lama seperti FileReader sebelum Java 18 memakai charset default sistem operasi, yang di Windows sering bukan UTF-8. File dari sistem lain juga belum tentu UTF-8, jadi charset adalah bagian dari kontrak format file.
Jebakan umum
- Lupa menutup
Files.lines/Files.walk. Stream-nya memegang file handle; tanpa try-with-resources server bisa kenaToo many open files. - Memuat file buatan pihak luar dengan
readString/readAllLines. Ukurannya tidak kamu kendalikan — proses secara streaming. - Mengandalkan path relatif. Working directory di server berbeda dengan di IDE. Ambil folder dasar dari konfigurasi dan jadikan absolut.
- Memercayai nama file upload.
resolvetidak menolak..atau path absolut — selalunormalize()+ cekstartsWith, atau buat nama sendiri. Files.copytanpaREPLACE_EXISTINGgagal denganFileAlreadyExistsExceptionbila target ada; sebaliknya,writeStringtanpaAPPENDdiam-diam menimpa isi lama.- Menganggap file temp terhapus sendiri.
createTempFiletidak membersihkan apa pun — hapus difinally.
Ringkasan
Pathadalah lokasi (tidak menyentuh disk);resolve,normalize, danrelativizemengolah path secara teks.Filesmenyediakan baca/tulis teks (readString,writeString,readAllLines) — selalu dengan charset UTF-8 eksplisit.- File besar diproses secara streaming dengan
Files.linesataunewBufferedReaderdi dalam try-with-resources. createDirectories,copy,move,delete,walk, dan file sementara melempar subclassIOExceptionyang spesifik sepertiNoSuchFileException.- Nama file dari klien tidak dipercaya: normalisasi + cek
startsWith(baseDir), atau buat nama sendiri.