I/O #
Hampir setiap aplikasi nyata perlu berinteraksi dengan dunia luar — membaca konfigurasi dari file, menyimpan laporan ke disk, mengimpor data CSV, atau mencatat log. Java punya dua generasi API untuk ini: java.io yang hadir sejak awal dan java.nio.file (NIO.2) yang diperkenalkan di Java 7 sebagai penyempurnaan besar. Keduanya masih relevan dan sering dipakai bersamaan. Artikel ini membahas cara membaca dan menulis file teks maupun biner, kapan memakai stream berbasis byte vs karakter, bagaimana NIO.2 menyederhanakan operasi file yang sebelumnya verbose, dan cara menyimpan objek Java ke disk lewat serialisasi.
Gambaran Umum #
Java punya dua lapisan API untuk I/O file:
| API | Paket | Diperkenalkan | Kelebihan |
|---|---|---|---|
| Classic I/O | java.io | Java 1.0 | Familiar, stream-based, cocok untuk data besar |
| NIO.2 | java.nio.file | Java 7 | Lebih ringkas, Files utility, dukungan metadata |
Untuk operasi file sehari-hari — baca, tulis, salin, hapus — NIO.2 (Files + Path) adalah pilihan yang lebih modern dan ringkas. Classic I/O masih dibutuhkan untuk streaming data besar, serialisasi, dan kelas-kelas seperti BufferedReader/BufferedWriter yang sering dipakai bersama NIO.2.
flowchart TD
A[Sumber Data] --> B{Tipe data?}
B -- "Biner\n(gambar, PDF, ZIP)" --> C["I/O Berbasis Byte\nInputStream / OutputStream"]
B -- "Teks\n(txt, csv, log)" --> D["I/O Berbasis Karakter\nReader / Writer"]
C --> E["FileInputStream\nFileOutputStream\nBufferedInputStream"]
D --> F["FileReader / FileWriter\nBufferedReader / BufferedWriter\nFiles.readString() / writeString()"]Selalu gunakan try-with-resources (try (Resource r = ...)) saat bekerja dengan stream dan file. Ini memastikan stream selalu ditutup setelah selesai — bahkan saat terjadi exception — tanpa perlu blokfinallymanual.
NIO.2 — Cara Modern Bekerja dengan File #
java.nio.file memperkenalkan Path (representasi path file/direktori) dan Files (kelas utilitas dengan metode statis untuk hampir semua operasi file). Banyak operasi yang butuh puluhan baris di java.io bisa diselesaikan dalam satu baris dengan Files.
Path — Merepresentasikan Lokasi File #
import java.nio.file.Path;
import java.nio.file.Paths;
// Membuat Path
Path file = Path.of("data/laporan.txt"); // Java 11+
Path fileOld = Paths.get("data", "laporan.txt"); // Java 7+, ekuivalen
// Navigasi path
Path dir = file.getParent(); // data
Path nama = file.getFileName(); // laporan.txt
Path abs = file.toAbsolutePath(); // /home/user/proyek/data/laporan.txt
// Gabung path dengan aman (bukan string concatenation)
Path subdir = Path.of("data").resolve("2025").resolve("laporan.txt");
// data/2025/laporan.txt
// Relativize: hitung path relatif antara dua path
Path dari = Path.of("/home/user/proyek");
Path ke = Path.of("/home/user/proyek/data/laporan.txt");
Path rel = dari.relativize(ke); // data/laporan.txt
Files — Operasi File dalam Satu Baris #
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.io.IOException;
Path src = Path.of("sumber.txt");
Path dst = Path.of("tujuan.txt");
Path dir = Path.of("direktori-baru");
// Cek keberadaan
boolean ada = Files.exists(src);
boolean adalahDir = Files.isDirectory(dir);
boolean bisaBaca = Files.isReadable(src);
// Metadata
long ukuran = Files.size(src); // dalam byte
var waktuUbah = Files.getLastModifiedTime(src);
// Buat dan hapus
Files.createFile(Path.of("baru.txt")); // buat file kosong
Files.createDirectory(dir); // buat satu direktori
Files.createDirectories(Path.of("a/b/c")); // buat bertingkat sekaligus
Files.delete(src); // hapus, lempar exception jika tidak ada
Files.deleteIfExists(src); // hapus jika ada, diam jika tidak
// Salin dan pindah
Files.copy(src, dst); // salin
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING); // timpa jika sudah ada
Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING); // pindah/rename
Membaca File Teks #
Untuk file teks, Java menyediakan beberapa cara dengan trade-off yang berbeda — pilih berdasarkan ukuran file dan kebutuhan.
Baca Seluruh File Sekaligus (File Kecil) #
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.charset.StandardCharsets;
import java.util.List;
Path file = Path.of("konfigurasi.txt");
// Baca seluruh isi sebagai String — Java 11+
String isi = Files.readString(file); // UTF-8 default
String isiLatin = Files.readString(file, StandardCharsets.ISO_8859_1);
// Baca sebagai List<String> (satu elemen per baris)
List<String> baris = Files.readAllLines(file);
baris.forEach(System.out::println);
// Baca sebagai byte array (untuk file biner kecil)
byte[] bytes = Files.readAllBytes(file);
readString()danreadAllLines()membaca seluruh file ke memori sekaligus. Untuk file yang besar (ratusan MB atau lebih), gunakanBufferedReaderdengan streaming agar tidak kehabisan heap.
Baca Baris per Baris dengan BufferedReader #
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
Path log = Path.of("server.log");
// try-with-resources: BufferedReader otomatis ditutup
try (BufferedReader br = Files.newBufferedReader(log)) {
String baris;
int nomor = 1;
while ((baris = br.readLine()) != null) {
System.out.printf("%4d: %s%n", nomor++, baris);
}
} catch (IOException e) {
System.err.println("Gagal membaca: " + e.getMessage());
}
Baca dengan Stream (Java 8+) #
import java.nio.file.Files;
import java.util.stream.Stream;
Path log = Path.of("server.log");
// Files.lines() mengembalikan Stream<String> — lazy, baris dibaca satu per satu
try (Stream<String> stream = Files.lines(log)) {
long jumlahError = stream
.filter(b -> b.contains("ERROR"))
.count();
System.out.println("Jumlah baris ERROR: " + jumlahError);
} catch (IOException e) {
e.printStackTrace();
}
// Ekstrak semua baris ERROR dan simpan ke list
try (Stream<String> stream = Files.lines(log)) {
List<String> errors = stream
.filter(b -> b.startsWith("ERROR"))
.collect(java.util.stream.Collectors.toList());
}
Menulis File Teks #
Tulis Sekaligus (File Kecil) #
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
Path output = Path.of("hasil.txt");
// Tulis String langsung — Java 11+, timpa jika sudah ada
Files.writeString(output, "Baris pertama\nBaris kedua\n");
// Tulis dengan opsi: APPEND untuk tambah di akhir file
Files.writeString(output, "Baris tambahan\n", StandardOpenOption.APPEND);
// Tulis List<String> (setiap elemen jadi satu baris)
List<String> baris = List.of("Apel", "Mangga", "Jeruk");
Files.write(output, baris);
Tulis dengan BufferedWriter #
Untuk output yang dihasilkan secara bertahap — misalnya menulis ribuan baris dalam loop — BufferedWriter jauh lebih efisien karena mengelompokkan banyak operasi tulis kecil menjadi satu operasi disk yang besar.
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
Path output = Path.of("laporan.csv");
// BufferedWriter — tulis baris per baris
try (BufferedWriter bw = Files.newBufferedWriter(output)) {
bw.write("nama,nilai,grade");
bw.newLine();
bw.write("Budi,85,A");
bw.newLine();
bw.write("Ani,72,B");
bw.newLine();
} catch (IOException e) {
e.printStackTrace();
}
// PrintWriter — alternatif dengan println() dan format()
try (PrintWriter pw = new PrintWriter(Files.newBufferedWriter(output))) {
pw.println("nama,nilai,grade");
pw.printf("%-10s,%3d,%s%n", "Budi", 85, "A");
pw.printf("%-10s,%3d,%s%n", "Ani", 72, "B");
}
// Append ke file yang sudah ada
try (BufferedWriter bw = Files.newBufferedWriter(output, StandardOpenOption.APPEND)) {
bw.write("Citra,91,A+");
bw.newLine();
}
I/O Berbasis Byte #
Untuk data biner — gambar, PDF, file audio, data terkompresi — gunakan InputStream/OutputStream. Operasi ini bekerja pada level byte, bukan karakter.
Membaca File Biner #
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
// Cara 1: Files.readAllBytes() — untuk file kecil
byte[] gambar = Files.readAllBytes(Path.of("foto.jpg"));
System.out.println("Ukuran: " + gambar.length + " byte");
// Cara 2: BufferedInputStream — untuk file besar, baca per chunk
Path input = Path.of("video-besar.mp4");
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream(input.toFile()))) {
byte[] buffer = new byte[8192]; // 8 KB per baca
int byteTerbaca;
long totalByte = 0;
while ((byteTerbaca = bis.read(buffer)) != -1) {
// proses buffer[0..byteTerbaca-1]
totalByte += byteTerbaca;
}
System.out.println("Total dibaca: " + totalByte + " byte");
} catch (IOException e) {
e.printStackTrace();
}
Menulis File Biner #
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
// Cara 1: Files.write() — untuk data byte[] yang sudah ada di memori
byte[] data = {0x48, 0x65, 0x6C, 0x6C, 0x6F}; // "Hello" dalam ASCII
Files.write(Path.of("output.bin"), data);
// Cara 2: BufferedOutputStream — untuk streaming data besar
Path output = Path.of("salinan.mp4");
Path sumber = Path.of("video.mp4");
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(sumber.toFile()));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(output.toFile()))) {
byte[] buffer = new byte[8192];
int byteTerbaca;
while ((byteTerbaca = bis.read(buffer)) != -1) {
bos.write(buffer, 0, byteTerbaca);
}
System.out.println("Salin selesai.");
} catch (IOException e) {
e.printStackTrace();
}
// Cara 3: Files.copy() — cara paling ringkas untuk menyalin file
Files.copy(sumber, output, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
Serialisasi Objek #
Serialisasi adalah proses mengubah objek Java menjadi urutan byte yang bisa disimpan ke file atau dikirim lewat jaringan, lalu dipulihkan kembali menjadi objek (deserialisasi). Kelas harus mengimplementasikan Serializable untuk bisa diserialisasi.
Mendefinisikan Kelas Serializable #
import java.io.Serializable;
public class Produk implements Serializable {
// serialVersionUID: identitas versi kelas untuk kompatibilitas deserialisasi
// Selalu definisikan ini secara eksplisit
private static final long serialVersionUID = 1L;
private String nama;
private double harga;
private int stok;
// Field dengan 'transient' TIDAK akan diserialisasi
private transient String cacheSementara;
public Produk(String nama, double harga, int stok) {
this.nama = nama;
this.harga = harga;
this.stok = stok;
}
@Override
public String toString() {
return "Produk{nama='%s', harga=%.2f, stok=%d}".formatted(nama, harga, stok);
}
}
Menulis Objek ke File #
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
List<Produk> katalog = List.of(
new Produk("Laptop", 12_000_000, 5),
new Produk("Mouse", 150_000, 20),
new Produk("Keyboard", 450_000, 15)
);
Path file = Path.of("katalog.dat");
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(Files.newOutputStream(file)))) {
oos.writeObject(katalog); // tulis seluruh list sekaligus
System.out.println("Katalog tersimpan.");
} catch (IOException e) {
e.printStackTrace();
}
Membaca Objek dari File #
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
Path file = Path.of("katalog.dat");
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(Files.newInputStream(file)))) {
@SuppressWarnings("unchecked")
List<Produk> katalog = (List<Produk>) ois.readObject();
katalog.forEach(System.out::println);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
Serialisasi bawaan Java punya beberapa keterbatasan: format tidak human-readable, rentan terhadap masalah kompatibilitas versi, dan bisa menjadi celah keamanan jika menerima data serialisasi dari sumber tidak tepercaya. Untuk keperluan persistensi modern, pertimbangkan format JSON (dengan Jackson atau Gson) atau database.
Operasi Direktori #
Membuat, Membaca, dan Menghapus Direktori #
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
Path dir = Path.of("data/arsip/2025");
// Buat direktori beserta induknya sekaligus
Files.createDirectories(dir);
// Daftar isi direktori (satu level)
try (var stream = Files.list(Path.of("data"))) {
stream.forEach(p -> System.out.println(p.getFileName()));
}
// Daftar isi direktori secara rekursif (semua subdirektori)
try (var stream = Files.walk(Path.of("data"))) {
stream
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".txt"))
.forEach(System.out::println);
}
// Hapus direktori kosong
Files.delete(dir);
// Hapus direktori beserta seluruh isinya (rekursif)
Files.walkFileTree(Path.of("data"), new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path d, IOException e) throws IOException {
Files.delete(d);
return FileVisitResult.CONTINUE;
}
});
Mencari File dengan glob #
// Cari semua file .java di seluruh proyek
try (var stream = Files.find(Path.of("src"), Integer.MAX_VALUE,
(path, attrs) -> attrs.isRegularFile() && path.toString().endsWith(".java"))) {
stream.forEach(System.out::println);
}
// Gunakan PathMatcher dengan glob pattern
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**.{java,kt}");
try (var stream = Files.walk(Path.of("src"))) {
stream
.filter(p -> matcher.matches(p))
.forEach(System.out::println);
}
Kasus Nyata #
Membaca File CSV #
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
record Mahasiswa(String nama, String jurusan, double ipk) {}
public static List<Mahasiswa> bacaCSV(Path file) throws IOException {
try (var lines = Files.lines(file)) {
return lines
.skip(1) // lewati header
.filter(l -> !l.isBlank())
.map(l -> {
String[] kolom = l.split(",");
return new Mahasiswa(
kolom[0].trim(),
kolom[1].trim(),
Double.parseDouble(kolom[2].trim())
);
})
.collect(Collectors.toList());
}
}
// CSV: nama,jurusan,ipk
// Budi,Informatika,3.8
// Ani,Matematika,3.5
List<Mahasiswa> data = bacaCSV(Path.of("mahasiswa.csv"));
data.forEach(System.out::println);
Menulis Laporan ke File #
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public static void tulisLaporan(Path output, List<Mahasiswa> data) throws IOException {
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm");
try (PrintWriter pw = new PrintWriter(Files.newBufferedWriter(output))) {
pw.println("=== LAPORAN MAHASISWA ===");
pw.println("Dicetak: " + LocalDateTime.now().format(fmt));
pw.println("-".repeat(40));
pw.printf("%-20s %-15s %s%n", "Nama", "Jurusan", "IPK");
pw.println("-".repeat(40));
data.forEach(m ->
pw.printf("%-20s %-15s %.2f%n", m.nama(), m.jurusan(), m.ipk())
);
pw.println("-".repeat(40));
double rataIpk = data.stream().mapToDouble(Mahasiswa::ipk).average().orElse(0);
pw.printf("Rata-rata IPK: %.2f%n", rataIpk);
}
}
Menyalin Semua File dari Satu Direktori ke Direktori Lain #
public static void salinSemua(Path src, Path dst) throws IOException {
Files.createDirectories(dst);
try (var stream = Files.list(src)) {
stream
.filter(Files::isRegularFile)
.forEach(file -> {
try {
Files.copy(file, dst.resolve(file.getFileName()),
StandardCopyOption.REPLACE_EXISTING);
System.out.println("Disalin: " + file.getFileName());
} catch (IOException e) {
System.err.println("Gagal salin " + file + ": " + e.getMessage());
}
});
}
}
Kapan Menggunakan Tiap Pendekatan #
Gunakan Files.readString() / writeString() jika:
✓ File teks kecil (< beberapa MB) yang seluruhnya perlu dimuat ke memori
✓ Mau kode yang paling ringkas dan mudah dibaca
Gunakan BufferedReader / BufferedWriter jika:
✓ File besar yang harus diproses baris per baris tanpa muat semua ke memori
✓ Output yang dihasilkan secara bertahap dalam loop
Gunakan Files.lines() dengan Stream jika:
✓ Perlu filter, map, atau reduce baris file secara fungsional
✓ Ingat: selalu tutup stream dengan try-with-resources
Gunakan FileInputStream / OutputStream jika:
✓ Data biner: gambar, audio, video, PDF, file terkompresi
✓ Tambahkan BufferedInputStream/OutputStream untuk performa lebih baik
Gunakan Serialisasi jika:
✓ Perlu simpan dan pulihkan objek Java dengan struktur kompleks
✗ Hindari untuk komunikasi antar sistem — pakai JSON/XML sebagai gantinya
Gunakan Files.copy() / move() jika:
✓ Salin atau pindahkan file — jauh lebih ringkas dari baca-tulis manual
Ringkasan #
- NIO.2 (
Path+Files) adalah cara modern —Files.readString(),writeString(),copy(),move(),createDirectories()menggantikan banyak kode verbose darijava.iolama.- Selalu gunakan try-with-resources —
try (var r = ...)memastikan stream selalu ditutup, bahkan saat exception. Lupa menutup stream adalah sumber bug dan kebocoran resource yang umum.BufferedReader/BufferedWriteruntuk file besar —readAllLines()danreadString()memuat seluruh file ke memori. Untuk file besar, baca per baris denganBufferedReaderatau stream denganFiles.lines().BufferedInputStream/BufferedOutputStreamuntuk I/O byte — tanpa buffering, setiapread()/write()memerlukan satu syscall ke OS. Dengan buffer 8 KB, ribuan operasi kecil dikelompokkan menjadi satu — perbedaan performa bisa 10–100×.Files.lines()harus ditutup — ia mengembalikanStream<String>yang memegang file handle terbuka. Selalu bungkus dalam try-with-resources.- Serialisasi Java untuk data internal — mudah dipakai tapi tidak portabel dan rawan masalah versi. Untuk API dan persistensi jangka panjang, gunakan JSON atau format lain yang lebih interoperable.
transientmengecualikan field dari serialisasi — gunakan untuk field yang tidak perlu atau tidak bisa diserialisasi (cache, koneksi database, dll.).Files.walk()untuk operasi rekursif — lebih aman dan ringkas dari iterasi manual. GunakanFiles.walkFileTree()denganSimpleFileVisitorsaat perlu kontrol lebih seperti hapus direktori secara rekursif.