IO #
Input dan Output adalah operasi yang ada di hampir semua aplikasi — membaca file konfigurasi, menulis log, memproses CSV, atau menyimpan hasil komputasi. Java menyediakan dua keluarga API untuk I/O: java.io yang berbasis stream dan sudah ada sejak Java 1, dan java.nio (New I/O) yang diperkenalkan di Java 4 dan diperbarui besar-besaran di Java 7 dengan NIO.2. Untuk hampir semua kebutuhan file modern, NIO.2 melalui java.nio.file.Files dan java.nio.file.Path adalah pilihan yang tepat — lebih ekspresif, lebih aman, dan dilengkapi method utility yang menyelesaikan pekerjaan umum dalam satu baris. API java.io masih relevan untuk streaming dan chaining decorator, tapi untuk manipulasi file dan path, NIO.2 jauh lebih baik. Artikel ini membahas keduanya secara mendalam, dengan penekanan pada pola yang benar dan anti-pattern yang sering menyebabkan bug dan resource leak.
Dua Keluarga API I/O #
Sebelum menulis kode, pahami kapan menggunakan masing-masing:
java.io
├── InputStream / OutputStream ← byte stream (binary)
├── Reader / Writer ← character stream (teks)
├── FileInputStream/FileOutputStream← akses file level rendah
├── BufferedReader/BufferedWriter ← buffering untuk performa
└── File ← representasi path (legacy)
java.nio.file (NIO.2 — Java 7+)
├── Path ← representasi path (modern)
├── Paths / Path.of() ← factory untuk membuat Path
├── Files ← utility operasi file (satu baris!)
└── FileSystem / FileSystems ← abstraksi filesystem
flowchart TD
TASK{Kebutuhan}
TASK -->|Baca/tulis file teks\natau binary sederhana| NIO2[NIO.2 — Files + Path\nPilihan utama]
TASK -->|Stream processing\ndecorator pattern| JAVAIO[java.io\nInputStream/Reader]
TASK -->|Non-blocking I/O\nribuan koneksi| NIO[java.nio\nChannel + Selector]
TASK -->|Operasi path\nmanipulasi direktori| NIO2
NIO2 -->|jika butuh streaming| JAVAIOPath — Representasi Lokasi File #
Path adalah cara modern Java merepresentasikan lokasi di filesystem — menggantikan java.io.File yang lama.
import java.nio.file.Path;
import java.nio.file.Paths;
// Membuat Path (Java 11+: Path.of() lebih idiomatis dari Paths.get())
Path p1 = Path.of("/home/user/dokumen/laporan.txt"); // absolute
Path p2 = Path.of("data", "input", "berkas.csv"); // relative, cross-platform
Path p3 = Paths.get("/tmp/cache"); // cara lama, masih valid
// Navigasi path
Path dir = Path.of("/home/user/proyek");
Path file = dir.resolve("src/Main.java"); // gabungkan path
// "/home/user/proyek/src/Main.java"
Path parent = file.getParent(); // "/home/user/proyek/src"
Path namaFile = file.getFileName(); // "Main.java"
Path root = file.getRoot(); // "/" (Unix) atau "C:\" (Windows)
// Informasi path
int dalamanan = file.getNameCount(); // jumlah segmen
Path segmen = file.getName(2); // segmen ke-2 (0-based)
// Normalisasi dan absolutisasi
Path kotor = Path.of("/home/user/../user/./dokumen");
Path bersih = kotor.normalize(); // "/home/user/dokumen"
Path absolut = Path.of("relatif/path").toAbsolutePath(); // dari current working dir
// Relativize — hitung path relatif antara dua path
Path basis = Path.of("/home/user");
Path tujuan = Path.of("/home/user/dokumen/laporan.txt");
Path relatif = basis.relativize(tujuan); // "dokumen/laporan.txt"
// Komparasi
boolean sama = p1.equals(p2); // bandingkan nilai path
boolean dimulaiDengan = file.startsWith(dir); // true
// Konversi ke URI dan File (untuk interop dengan API lama)
java.net.URI uri = file.toUri(); // "file:///home/user/..."
java.io.File fileObj = file.toFile(); // interop dengan java.io
// Cross-platform — Path.of() otomatis gunakan separator yang benar
Path crossPlatform = Path.of("data", "output", "hasil.csv");
// Windows: "data\output\hasil.csv"
// Unix: "data/output/hasil.csv"
Files — Operasi File Satu Baris #
Files adalah utility class berisi method statik untuk semua operasi file umum. Ini adalah API yang harus kamu gunakan untuk sebagian besar kebutuhan file.
Baca dan Tulis File Teks #
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class OperasiFileTeks {
public void demo() throws Exception {
Path filePath = Path.of("data/laporan.txt");
// ===== MEMBACA =====
// Baca seluruh isi sebagai String — untuk file kecil
String isi = Files.readString(filePath); // Java 11+
String isiCharset = Files.readString(filePath, StandardCharsets.UTF_8);
// Baca semua baris sebagai List<String>
List<String> baris = Files.readAllLines(filePath); // UTF-8 default
List<String> barisLatin = Files.readAllLines(filePath,
StandardCharsets.ISO_8859_1);
// Baca sebagai Stream<String> — untuk file besar (lazy, tidak load semua ke memory)
try (var stream = Files.lines(filePath)) {
stream
.filter(b -> !b.isBlank())
.map(String::trim)
.forEach(System.out::println);
} // stream ditutup otomatis
// Baca sebagai byte array — untuk file binary kecil
byte[] bytes = Files.readAllBytes(filePath);
// ===== MENULIS =====
// Tulis String ke file (buat baru atau timpa yang ada)
Files.writeString(filePath, "Konten baru"); // Java 11+
Files.writeString(filePath, "Konten UTF-8", StandardCharsets.UTF_8);
// Tulis dengan opsi tambahan
Files.writeString(filePath, "Baris tambahan\n",
StandardOpenOption.APPEND); // tambah ke akhir file
Files.writeString(filePath, "Hanya jika belum ada",
StandardOpenOption.CREATE_NEW); // gagal jika file sudah ada
// Tulis List<String> sebagai baris
List<String> konten = List.of("baris 1", "baris 2", "baris 3");
Files.write(filePath, konten);
Files.write(filePath, konten, StandardCharsets.UTF_8,
StandardOpenOption.APPEND);
// Tulis byte array
byte[] data = "data binary".getBytes(StandardCharsets.UTF_8);
Files.write(filePath, data);
}
}
Operasi File dan Direktori #
public class ManajemenFile {
public void demo() throws Exception {
Path src = Path.of("sumber.txt");
Path dst = Path.of("tujuan.txt");
Path dir = Path.of("direktori/baru");
// ===== CEK KEBERADAAN DAN ATRIBUT =====
boolean ada = Files.exists(src);
boolean tidakAda = Files.notExists(src);
boolean adaFile = Files.isRegularFile(src);
boolean adaDir = Files.isDirectory(dir);
boolean bisaDibaca = Files.isReadable(src);
boolean bisaDitulis = Files.isWritable(src);
boolean bisaDieksekusi = Files.isExecutable(src);
long ukuran = Files.size(src); // dalam bytes
java.time.Instant diubah = Files.getLastModifiedTime(src).toInstant();
// ===== COPY DAN MOVE =====
// Copy file
Files.copy(src, dst); // gagal jika dst sudah ada
Files.copy(src, dst,
java.nio.file.StandardCopyOption.REPLACE_EXISTING, // timpa jika ada
java.nio.file.StandardCopyOption.COPY_ATTRIBUTES); // salin atribut file
// Move / rename file
Files.move(src, dst);
Files.move(src, dst,
java.nio.file.StandardCopyOption.REPLACE_EXISTING,
java.nio.file.StandardCopyOption.ATOMIC_MOVE); // atomic pada filesystem yang mendukung
// ===== HAPUS =====
Files.delete(src); // throw exception jika tidak ada
Files.deleteIfExists(src); // tidak throw jika tidak ada
// ===== DIREKTORI =====
Files.createDirectory(dir); // buat satu level direktori
Files.createDirectories(dir); // buat semua level (mkdir -p)
// File sementara
Path tmpFile = Files.createTempFile("prefix-", ".tmp");
Path tmpDir = Files.createTempDirectory("tmpdir-");
// INGAT: hapus file sementara setelah selesai
tmpFile.toFile().deleteOnExit(); // hapus saat JVM keluar
}
}
Membaca File dengan BufferedReader #
Untuk file teks berukuran besar yang perlu diproses baris per baris, BufferedReader lebih efisien dari Files.readAllLines() karena tidak memuat seluruh file ke memori sekaligus.
import java.io.*;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
public class BufferedReaderDemo {
// ✗ ANTI-PATTERN: FileReader tanpa charset — bergantung platform default
public void bacaTanpaCharset(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
String baris;
while ((baris = reader.readLine()) != null) {
System.out.println(baris);
}
}
}
// ✓ BENAR: selalu tentukan charset eksplisit
public void bacaDenganCharset(Path path) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String baris;
while ((baris = reader.readLine()) != null) {
// proses baris
}
}
// try-with-resources otomatis menutup reader meski terjadi exception
}
// ✓ LEBIH MODERN: Files.lines() dengan Stream API
public long hitungBarisNonKosong(Path path) throws IOException {
try (var lines = Files.lines(path, StandardCharsets.UTF_8)) {
return lines.filter(b -> !b.isBlank()).count();
}
}
// Proses file CSV besar tanpa muat semua ke memory
public void prosesCSVBesar(Path csvPath) throws IOException {
try (var lines = Files.lines(csvPath, StandardCharsets.UTF_8)) {
lines
.skip(1) // lewati header
.map(baris -> baris.split(",")) // pecah kolom
.filter(kolom -> kolom.length >= 3)
.forEach(kolom -> {
String nama = kolom[0].trim();
String harga = kolom[1].trim();
System.out.println("Produk: " + nama + " | Harga: " + harga);
});
}
}
}
Menulis File dengan BufferedWriter #
import java.io.*;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
public class BufferedWriterDemo {
// ✓ BENAR: BufferedWriter dengan charset eksplisit
public void tulisDenganBuffer(Path path, List<String> data) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
for (String baris : data) {
writer.write(baris);
writer.newLine(); // \n atau \r\n sesuai OS
}
}
}
// Append ke file yang sudah ada
public void tambahKanLog(Path logPath, String pesan) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(
logPath,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE, // buat jika belum ada
StandardOpenOption.APPEND)) // tambah ke akhir
{
writer.write(java.time.LocalDateTime.now() + " | " + pesan);
writer.newLine();
}
}
// PrintWriter — lebih nyaman untuk output terformat
public void tulisLaporan(Path path) throws IOException {
try (PrintWriter pw = new PrintWriter(
Files.newBufferedWriter(path, StandardCharsets.UTF_8))) {
pw.println("=== Laporan Penjualan ===");
pw.printf("Tanggal: %tF%n", java.time.LocalDate.now());
pw.printf("Total: Rp %.2f%n", 15000000.0);
pw.flush(); // pastikan semua data ditulis
}
}
}
Membaca dan Menulis File Binary #
Untuk file binary (gambar, PDF, arsip, data serialisasi), gunakan stream byte tanpa karakter encoding.
import java.io.*;
import java.nio.file.*;
public class FileBinaryDemo {
// Baca file binary (untuk file kecil — muat semua ke memory)
public byte[] bacaFileBinary(Path path) throws IOException {
return Files.readAllBytes(path); // semua byte sekaligus
}
// Baca file binary besar dengan buffer
public void prosesFileBesar(Path src, Path dst) throws IOException {
try (InputStream in = new BufferedInputStream(Files.newInputStream(src));
OutputStream out = new BufferedOutputStream(Files.newOutputStream(dst))) {
byte[] buffer = new byte[8192]; // 8 KB buffer
int jumlahBaca;
while ((jumlahBaca = in.read(buffer)) != -1) {
out.write(buffer, 0, jumlahBaca);
}
}
// Keduanya ditutup otomatis — bahkan jika terjadi exception di tengah proses
}
// Transfer file — cara paling efisien menggunakan OS-level copy
public void salinFile(Path src, Path dst) throws IOException {
try (InputStream in = Files.newInputStream(src);
OutputStream out = Files.newOutputStream(dst)) {
in.transferTo(out); // Java 9+ — delegasikan ke OS untuk efisiensi maksimal
}
}
// DataInputStream / DataOutputStream — baca/tulis tipe primitif ke binary stream
public void tulisDataBinary(Path path) throws IOException {
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(Files.newOutputStream(path)))) {
dos.writeInt(42);
dos.writeDouble(3.14);
dos.writeBoolean(true);
dos.writeUTF("halo"); // tulis String sebagai UTF-8 dengan length prefix
}
}
public void bacaDataBinary(Path path) throws IOException {
try (DataInputStream dis = new DataInputStream(
new BufferedInputStream(Files.newInputStream(path)))) {
int angka = dis.readInt(); // harus dibaca dalam urutan yang sama dengan penulisan!
double desimal = dis.readDouble();
boolean bool = dis.readBoolean();
String teks = dis.readUTF();
System.out.printf("int=%d, double=%.2f, bool=%b, str=%s%n",
angka, desimal, bool, teks);
}
}
}
Traversal Direktori #
Daftar Isi Direktori #
import java.nio.file.*;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
public class TraversalDirektori {
// Daftar isi direktori (satu level saja)
public List<Path> daftarIsi(Path dir) throws IOException {
try (var stream = Files.list(dir)) {
return stream
.sorted() // urutkan berdasarkan nama
.toList();
}
}
// Filter hanya file tertentu
public List<Path> cariFileJava(Path dir) throws IOException {
try (var stream = Files.list(dir)) {
return stream
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.sorted()
.toList();
}
}
// Traversal rekursif seluruh pohon direktori
public List<Path> semuaFile(Path root) throws IOException {
try (var stream = Files.walk(root)) {
return stream
.filter(Files::isRegularFile)
.sorted()
.toList();
}
}
// walk dengan kedalaman maksimum
public List<Path> fileKedalaman2(Path root) throws IOException {
try (var stream = Files.walk(root, 2)) { // maks 2 level ke bawah
return stream
.filter(Files::isRegularFile)
.toList();
}
}
// Hitung total ukuran direktori
public long totalUkuranDir(Path dir) throws IOException {
try (var stream = Files.walk(dir)) {
return stream
.filter(Files::isRegularFile)
.mapToLong(p -> {
try { return Files.size(p); }
catch (IOException e) { return 0L; }
})
.sum();
}
}
// find() — traversal dengan filter yang lebih kuat
public List<Path> cariFileBesar(Path root, long ukuranMin) throws IOException {
try (var stream = Files.find(root, Integer.MAX_VALUE,
(path, attrs) ->
attrs.isRegularFile() &&
attrs.size() > ukuranMin)) {
return stream.toList();
}
}
// Hapus direktori beserta isinya (rekursif)
public void hapusDirektori(Path dir) throws IOException {
// Files.delete() gagal jika direktori tidak kosong
// Harus hapus isi dulu menggunakan walk dalam urutan terbalik
try (var stream = Files.walk(dir)) {
stream.sorted(java.util.Comparator.reverseOrder())
.forEach(path -> {
try { Files.delete(path); }
catch (IOException e) {
throw new RuntimeException("Gagal hapus: " + path, e);
}
});
}
}
// Salin direktori beserta isinya
public void salinDirektori(Path src, Path dst) throws IOException {
try (var stream = Files.walk(src)) {
stream.forEach(sumberPath -> {
Path tujuanPath = dst.resolve(src.relativize(sumberPath));
try {
if (Files.isDirectory(sumberPath)) {
Files.createDirectories(tujuanPath);
} else {
Files.copy(sumberPath, tujuanPath,
StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) {
throw new RuntimeException("Gagal salin: " + sumberPath, e);
}
});
}
}
}
File Attributes dan Metadata #
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.time.Instant;
public class AtributFileDemo {
public void bacaAtribut(Path path) throws Exception {
// Atribut dasar
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
System.out.println("Regular file: " + attrs.isRegularFile());
System.out.println("Directory: " + attrs.isDirectory());
System.out.println("Symbolic link: " + attrs.isSymbolicLink());
System.out.println("Ukuran: " + attrs.size() + " bytes");
System.out.println("Dibuat: " + attrs.creationTime().toInstant());
System.out.println("Diubah terakhir: " + attrs.lastModifiedTime().toInstant());
System.out.println("Diakses terakhir: " + attrs.lastAccessTime().toInstant());
// Ubah waktu modifikasi
Files.setLastModifiedTime(path,
FileTime.from(Instant.now()));
// POSIX atribut (Unix/Linux/Mac)
if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
PosixFileAttributes posix = Files.readAttributes(path, PosixFileAttributes.class);
System.out.println("Owner: " + posix.owner().getName());
System.out.println("Group: " + posix.group().getName());
System.out.println("Permissions: " + PosixFilePermissions.toString(posix.permissions()));
// Ubah permission (setara chmod 644)
Files.setPosixFilePermissions(path, PosixFilePermissions.fromString("rw-r--r--"));
}
}
}
WatchService — Pantau Perubahan File #
WatchService memungkinkan aplikasi bereaksi terhadap perubahan pada direktori — file baru ditambahkan, diubah, atau dihapus — tanpa perlu polling berulang.
import java.nio.file.*;
public class FileWatcherDemo {
public void pantauDirektori(Path dir) throws Exception {
WatchService watcher = FileSystems.getDefault().newWatchService();
// Daftar event yang ingin dipantau
dir.register(watcher,
StandardWatchEventKinds.ENTRY_CREATE, // file baru dibuat
StandardWatchEventKinds.ENTRY_MODIFY, // file diubah
StandardWatchEventKinds.ENTRY_DELETE // file dihapus
);
System.out.println("Memantau direktori: " + dir);
while (true) {
// take() — blokir sampai ada event
WatchKey key = watcher.take();
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> jenis = event.kind();
// OVERFLOW berarti beberapa event mungkin terlewat
if (jenis == StandardWatchEventKinds.OVERFLOW) {
System.out.println("Beberapa event mungkin terlewat!");
continue;
}
// Nama file yang berubah
@SuppressWarnings("unchecked")
WatchEvent<Path> pathEvent = (WatchEvent<Path>) event;
Path namaFile = pathEvent.context();
Path fullPath = dir.resolve(namaFile);
if (jenis == StandardWatchEventKinds.ENTRY_CREATE) {
System.out.println("File dibuat: " + fullPath);
} else if (jenis == StandardWatchEventKinds.ENTRY_MODIFY) {
System.out.println("File diubah: " + fullPath);
} else if (jenis == StandardWatchEventKinds.ENTRY_DELETE) {
System.out.println("File dihapus: " + fullPath);
}
}
// Reset key — wajib agar bisa menerima event berikutnya
boolean valid = key.reset();
if (!valid) {
System.out.println("Direktori tidak lagi dapat dipantau");
break;
}
}
}
}
Serialisasi Objek #
Java menyediakan mekanisme serialisasi bawaan untuk mengkonversi objek ke byte stream dan sebaliknya. Perlu diperhatikan bahwa serialisasi Java bawaan punya banyak kelemahan dan sebaiknya diganti dengan format yang lebih modern (JSON, Protobuf, Avro) untuk komunikasi antar sistem.
import java.io.*;
import java.nio.file.*;
// Kelas yang bisa diserialisasi harus implements Serializable
public class Produk implements Serializable {
// serialVersionUID penting! Jika tidak ada dan struktur kelas berubah,
// deserialisasi objek lama akan gagal dengan InvalidClassException
@Serial
private static final long serialVersionUID = 1L;
private String nama;
private double harga;
// Field yang tidak perlu diserialisasi — ditandai transient
private transient String cacheKey; // tidak akan diserialisasi
public Produk(String nama, double harga) {
this.nama = nama;
this.harga = harga;
}
// getter...
}
public class SerializasiDemo {
// Serialisasi — objek ke file
public void simpan(Produk produk, Path path) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(Files.newOutputStream(path)))) {
oos.writeObject(produk);
}
}
// Deserialisasi — file ke objek
public Produk muat(Path path) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(Files.newInputStream(path)))) {
return (Produk) ois.readObject();
}
}
}
Serialisasi Java bawaan (ObjectInputStream/ObjectOutputStream) memiliki risiko keamanan serius — deserialisasi data yang tidak terpercaya dapat menyebabkan Remote Code Execution. Untuk pertukaran data antar sistem atau penyimpanan jangka panjang, gunakan JSON (Jackson/Gson), Protobuf, atau format yang lebih modern. Serialisasi Java bawaan hanya aman jika data berasal dari sumber yang sepenuhnya terpercaya.
Pola Penanganan Resource yang Aman #
Try-with-Resources #
Selalu gunakan try-with-resources untuk memastikan resource ditutup meski terjadi exception:
// ✗ ANTI-PATTERN: tutup manual — resource leak jika exception terjadi sebelum close()
BufferedReader reader = null;
try {
reader = Files.newBufferedReader(path);
String baris = reader.readLine();
// jika exception di sini, reader.close() tidak pernah dipanggil!
} finally {
if (reader != null) {
reader.close(); // boilerplate dan rawan lupa
}
}
// ✓ BENAR: try-with-resources — close() dipanggil otomatis, bahkan saat exception
try (BufferedReader reader = Files.newBufferedReader(path)) {
String baris = reader.readLine();
// exception di sini? reader.close() tetap dipanggil!
}
// Multiple resources — ditutup dalam urutan terbalik (ois ditutup sebelum bis)
try (InputStream bis = new BufferedInputStream(Files.newInputStream(src));
ObjectInputStream ois = new ObjectInputStream(bis)) {
Object obj = ois.readObject();
}
// Resource kustom — implementasikan AutoCloseable
public class KoneksiDB implements AutoCloseable {
public KoneksiDB(String url) { /* buka koneksi */ }
@Override
public void close() {
/* tutup koneksi — dipanggil otomatis oleh try-with-resources */
System.out.println("Koneksi DB ditutup");
}
}
try (KoneksiDB db = new KoneksiDB("jdbc:postgresql://...")) {
// gunakan db
} // db.close() dipanggil otomatis
Kapan Menggunakan API yang Mana #
GUNAKAN Files (NIO.2) JIKA:
✓ Baca/tulis file teks — Files.readString(), Files.writeString()
✓ Baca/tulis binary sederhana — Files.readAllBytes(), Files.write()
✓ Manipulasi file/direktori — copy, move, delete, createDirectories
✓ Traversal direktori — Files.list(), Files.walk(), Files.find()
✓ Cek atribut dan metadata file
✓ Hampir semua kebutuhan file modern
GUNAKAN java.io Stream JIKA:
✓ File sangat besar yang tidak muat di memory — streaming line by line
✓ Chaining decorator (Buffer + Gzip + Cipher + stream)
✓ Serialisasi objek Java
✓ Interoperabilitas dengan API lama yang menggunakan InputStream/Reader
GUNAKAN WatchService JIKA:
✓ Aplikasi perlu bereaksi terhadap perubahan file secara real-time
✓ Hot reload konfigurasi tanpa restart aplikasi
✓ Pipeline yang diproses ketika file baru masuk ke direktori
Ringkasan #
FilesdanPath(NIO.2) adalah API modern yang harus digunakan untuk hampir semua operasi file —Files.readString(),Files.writeString(),Files.copy(),Files.walk()menyelesaikan pekerjaan umum dalam satu baris yang ekspresif.- Selalu gunakan try-with-resources untuk semua resource I/O —
BufferedReader,InputStream,Stream<Path>semuanyaAutoCloseabledan harus ditutup untuk mencegah file handle leak.- Tentukan charset secara eksplisit (
StandardCharsets.UTF_8) setiap kali bekerja dengan teks. Mengandalkan default platform menyebabkan bug yang sulit direproduksi di lingkungan berbeda.Files.lines()untuk file teks besar — mengembalikanStream<String>yang lazy, tidak memuat seluruh file ke memori sepertiFiles.readAllLines().BufferedInputStream/BufferedOutputStreamselalu wrapInputStream/OutputStreammentah untuk performa — operasi I/O tanpa buffer sangat lambat karena setiapread()/write()menjadi system call.Path.of()(Java 11+) menggantikanPaths.get()dannew File()— gunakan ini untuk semua representasi path baru. Cross-platform otomatis menggunakan separator yang benar.in.transferTo(out)(Java 9+) adalah cara paling efisien menyalin stream — delegasikan ke OS-level untuk menghindari copy buffer di Java.- Hindari serialisasi Java bawaan untuk data yang dikomunikasikan antar sistem atau disimpan jangka panjang — gunakan JSON, Protobuf, atau Avro yang lebih aman, portabel, dan mudah di-evolve.