Selama Section ini kita menguasai satu per satu alat I/O Java — stream, reader, buffer, channel, atribut, izin. Kode nyata jarang semudah contoh: berkas input bisa hilang atau berisi baris rusak, proses bisa terputus di tengah penulisan, dan hasil setengah jadi lebih buruk daripada tidak ada hasil sama sekali. Pemrosesan berkas yang tangguh adalah seni merangkai alat-alat itu menjadi alur yang tetap terhormat saat semuanya berjalan buruk: charset dinyatakan eksplisit, sumber daya ditutup pasti, exception yang spesifik ditangani dengan sengaja, input divalidasi sebelum dipercaya, dan hasil dipasang secara atomik lalu diverifikasi. Chapter penutup ini merangkai seluruhnya dalam satu pipeline lengkap.

Lima Prinsip Kode I/O yang Tangguh #

Sebelum melihat kode, mari tetapkan sasaran desainnya dalam lima prinsip. Pertama, charset eksplisit di setiap baca-tulis teks — StandardCharsets.UTF_8 ditulis di kode, bukan diwariskan ke keberuntungan locale. Kedua, try-with-resources untuk setiap sumber daya; stream dan reader yang bocor bukan hanya boros — pada Windows bisa mengunci berkas dari proses lain. Ketiga, tangkap exception yang paling spesifik lebih dulu: NoSuchFileException menuntaskan “input hilang”, NumberFormatException menuntaskan “data rusak”, dan IOException umum menjadi jaring terakhir — setiap jenis digiring ke penanganan yang berbeda. Keempat, validasi sebelum percaya: baris yang tidak lolos pemeriksaan bentuk dikumpulkan terpisah, bukan dibiarkan meracuni hasil. Kelima, pasang hasil secara atomik: tulis ke berkas sementara di direktori yang sama, lalu Files.move dengan ATOMIC_MOVE — pembaca tidak pernah melihat berkas hasil yang setengah tertulis.

Studi Kasus: Pipeline Ringkasan Peserta #

Berkas pendamping peserta.csv sengaja memuat dua baris bermasalah: Dewi tanpa kolom skor, dan Ani,seratus dengan skor bukan angka. Pipeline kita membaca sumber dengan charset eksplisit, memilah baris valid dari yang ditolak, menulis hasil ke ringkasan.tmp, memasangnya sebagai ringkasan.csv secara atomik, lalu membaca balik untuk verifikasi. Konfigurasi ambang kelulusan dibaca dari konfigurasi.txt yang tidak tersedia — hilangnya berkas opsional harusnya tidak menghentikan pemrosesan:

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;

public class ProsesPeserta {
    public static void main(String[] args) {
        Path sumber = Path.of("peserta.csv");
        Path hasil = Path.of("ringkasan.csv");
        Path sementara = Path.of("ringkasan.tmp");

        int batas = bacaBatas();

        List<String> valid = new ArrayList<>();
        List<String> ditolak = new ArrayList<>();
        try (BufferedReader pembaca =
                     Files.newBufferedReader(sumber, StandardCharsets.UTF_8)) {
            pembaca.readLine(); // buang baris judul kolom
            String baris;
            while ((baris = pembaca.readLine()) != null) {
                if (baris.isBlank()) {
                    continue; // baris kosong diabaikan diam-diam
                }
                String[] kolom = baris.split(",");
                try {
                    int skor = Integer.parseInt(kolom[1].trim());
                    if (skor < 0) {
                        ditolak.add(baris);
                    } else {
                        valid.add(baris);
                    }
                } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
                    ditolak.add(baris);
                }
            }
        } catch (NoSuchFileException e) {
            System.out.println("sumber hilang : " + e.getFile());
            return;
        } catch (IOException e) {
            System.out.println("baca gagal    : " + e.getClass().getSimpleName());
            return;
        }

        System.out.println("konfigurasi   : batas=" + batas);
        System.out.println("valid         : " + valid.size());
        System.out.println("ditolak       : " + ditolak.size());

        try (BufferedWriter penulis =
                     Files.newBufferedWriter(sementara, StandardCharsets.UTF_8)) {
            penulis.write("nama,score,hasil\n");
            for (String baris : valid) {
                String[] kolom = baris.split(",");
                int skor = Integer.parseInt(kolom[1].trim());
                String keputusan = skor >= batas ? "lulus" : "ulang";
                penulis.write(kolom[0] + "," + skor + "," + keputusan + "\n");
            }
        } catch (IOException e) {
            System.out.println("tulis gagal   : " + e.getClass().getSimpleName());
            return;
        }

        // Pasang hasil secara atomik; jatuh ke cadangan bila tidak didukung.
        try {
            Files.move(sementara, hasil, StandardCopyOption.ATOMIC_MOVE);
            System.out.println("pasang        : ATOMIC_MOVE");
        } catch (AtomicMoveNotSupportedException e) {
            try {
                Files.move(sementara, hasil, StandardCopyOption.REPLACE_EXISTING);
                System.out.println("pasang        : fallback REPLACE_EXISTING");
            } catch (IOException e2) {
                System.out.println("pasang gagal  : " + e2.getClass().getSimpleName());
                return;
            }
        } catch (IOException e) {
            System.out.println("pasang gagal  : " + e.getClass().getSimpleName());
            return;
        }

        // Verifikasi akhir: baca balik hasil dan hitung barisnya.
        try (BufferedReader pembaca =
                     Files.newBufferedReader(hasil, StandardCharsets.UTF_8)) {
            System.out.println("-- ringkasan.csv --");
            pembaca.lines().forEach(b -> System.out.println("  " + b));
            long jumlah = Files.lines(hasil, StandardCharsets.UTF_8).count();
            System.out.println("verifikasi    : " + jumlah
                    + " baris (1 judul + " + valid.size() + " data)");
        } catch (IOException e) {
            System.out.println("verifikasi gagal: " + e.getClass().getSimpleName());
        }
    }

    // Konfigurasi opsional: hilang atau rusak tidak membatalkan proses.
    static int bacaBatas() {
        try {
            String isi = Files.readString(Path.of("konfigurasi.txt"));
            return Integer.parseInt(isi.trim());
        } catch (IOException | NumberFormatException e) {
            return 60;
        }
    }
}

Output yang Dihasilkan:

text
konfigurasi   : batas=60
valid         : 3
ditolak       : 2
pasang        : ATOMIC_MOVE
-- ringkasan.csv --
  nama,score,hasil
  Budi,90,lulus
  Siti,85,lulus
  Rudi,78,lulus
verifikasi    : 4 baris (1 judul + 3 data)

Mari kita bedah keputusan-keputusan pentingnya:

  1. Baris 21 (bacaBatas() lebih dulu): konfigurasi dibaca sebelum loop utama. bacaBatas menangkap IOException (termasuk NoSuchFileException yang merupakan subclass-nya) dan NumberFormatException dalam satu multi-catch lalu mengembalikan nilai standar 60 — berkas opsional yang hilang atau isinya rusak tidak boleh membatalkan seluruh proses, dan keluaran membuktikannya lewat batas=60.
  2. Baris 24-46 (validasi berlapis): baris kosong diabaikan; setiap baris dipecah dan Integer.parseInt dijaga try-catch kecil — NumberFormatException untuk seratus, ArrayIndexOutOfBoundsException untuk baris tanpa kolom kedua. Keduanya berujung pada daftar ditolak yang terpisah, sehingga data rusak tercatat, bukan menghentikan atau diselundupkan ke hasil.
  3. Baris 47-53 (dua catch berjenjang): NoSuchFileException ditangkap sebelum IOException karena lebih spesifik — compiler menolak urutan terbalik. Keduanya menghentikan program dengan pesan berbeda: hilangnya sumber adalah kondisi lain dari kegagalan baca di tengah jalan.
  4. Baris 62-71 (tulis ke sementara): hasil ditulis ke ringkasan.tmp, bukan langsung ke ringkasan.csv. Selama penulisan berlangsung, berkas hasil yang lama tetap utuh — pembaca lain tidak pernah melihat ringkasan.csv yang setengah jadi.
  5. Baris 73-89 (pasangan ATOMIC_MOVE): Files.move dengan StandardCopyOption.ATOMIC_MOVE mengganti ringkasan.tmp menjadi ringkasan.csv dalam satu operasi rename kernel yang tidak bisa diam setengah jalan. Bila sistem berkas menolak — AtomicMoveNotSupportedException — fallback REPLACE_EXISTING tetap menjalankan tugas dengan jaminan lebih lemah; program mencatat jalur mana yang dipakai sehingga perilakunya terlihat.
  6. Baris 91-102 (verifikasi baca-balik): hasil dibaca kembali dan jumlah barisnya dihitung; kecocokan “1 judul + 3 data” menutup siklus tulis-lalu-periksa. Bila proses ini dijalankan ulang, hasil yang sama tertulis kembali — alur idempoten yang tidak menumpuk efek samping.

Pulihkan Input yang Hilang dengan Elegan #

Satu pola penutup yang sering dipakai untuk berkas pengaturan: input hilang bukanlah error, melainkan ajak menciptakan sesuatu. Program berikut membaca pengaturan.txt; karena berkas itu belum ada, program menuliskan nilai bawaan dan melanjutkan seolah tidak terjadi apa-apa — dan pada eksekusi berikutnya berkas itu sudah ada:

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;

public class PulihkanInput {
    public static void main(String[] args) throws IOException {
        Path input = Path.of("pengaturan.txt");
        String isi;

        try {
            isi = Files.readString(input, StandardCharsets.UTF_8);
            System.out.println("sumber        : berkas");
        } catch (NoSuchFileException e) {
            isi = "mode=wajar\n";
            Files.writeString(input, isi, StandardCharsets.UTF_8);
            System.out.println("sumber        : bawaan, berkas dibuat");
        }
        System.out.println("isi           : " + isi.lines().findFirst().orElse(""));
    }
}

Output yang Dihasilkan:

text
sumber        : bawaan, berkas dibuat
isi           : mode=wajar

Program pendek ini memperlihatkan pola self-healing yang sehat: NoSuchFileException ditangkap tepat untuk satu tujuan — mengisi nilai bawaan — lalu eksekusi berlanjut ke jalur yang sama dengan kasus sukses. Tidak ada return mendadak, tidak ada stack trace ke terminal pengguna, dan berkas yang tercipta membuat eksekusi berikutnya memasuki cabang “berkas” secara alami.

Dengan ini perjalanan kita di dunia I/O Java tuntas: byte mentah, karakter dan charset, buffer dan channel, metadata, izin, pemantauan perubahan, hingga pipeline transaksional. Semua contoh sejauh ini berjalan satu utas — padahal I/O justru wilayah yang paling diuntungkan eksekusi paralel: menunggu disk dan jaringan adalah waktu luang yang bisa dipakai mengerjakan yang lain. Section berikutnya, Concurrency dan Multithreading, membuka kotak perkakas tersebut: thread, ExecutorService, hingga virtual threads untuk I/O yang masif.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

ProsesPeserta.java

Unduh berkas
java
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;

public class ProsesPeserta {
    public static void main(String[] args) {
        Path sumber = Path.of("peserta.csv");
        Path hasil = Path.of("ringkasan.csv");
        Path sementara = Path.of("ringkasan.tmp");

        int batas = bacaBatas();

        List<String> valid = new ArrayList<>();
        List<String> ditolak = new ArrayList<>();
        try (BufferedReader pembaca =
                     Files.newBufferedReader(sumber, StandardCharsets.UTF_8)) {
            pembaca.readLine(); // buang baris judul kolom
            String baris;
            while ((baris = pembaca.readLine()) != null) {
                if (baris.isBlank()) {
                    continue; // baris kosong diabaikan diam-diam
                }
                String[] kolom = baris.split(",");
                try {
                    int skor = Integer.parseInt(kolom[1].trim());
                    if (skor < 0) {
                        ditolak.add(baris);
                    } else {
                        valid.add(baris);
                    }
                } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
                    ditolak.add(baris);
                }
            }
        } catch (NoSuchFileException e) {
            System.out.println("sumber hilang : " + e.getFile());
            return;
        } catch (IOException e) {
            System.out.println("baca gagal    : " + e.getClass().getSimpleName());
            return;
        }

        System.out.println("konfigurasi   : batas=" + batas);
        System.out.println("valid         : " + valid.size());
        System.out.println("ditolak       : " + ditolak.size());

        try (BufferedWriter penulis =
                     Files.newBufferedWriter(sementara, StandardCharsets.UTF_8)) {
            penulis.write("nama,score,hasil\n");
            for (String baris : valid) {
                String[] kolom = baris.split(",");
                int skor = Integer.parseInt(kolom[1].trim());
                String keputusan = skor >= batas ? "lulus" : "ulang";
                penulis.write(kolom[0] + "," + skor + "," + keputusan + "\n");
            }
        } catch (IOException e) {
            System.out.println("tulis gagal   : " + e.getClass().getSimpleName());
            return;
        }

        // Pasang hasil secara atomik; jatuh ke cadangan bila tidak didukung.
        try {
            Files.move(sementara, hasil, StandardCopyOption.ATOMIC_MOVE);
            System.out.println("pasang        : ATOMIC_MOVE");
        } catch (AtomicMoveNotSupportedException e) {
            try {
                Files.move(sementara, hasil, StandardCopyOption.REPLACE_EXISTING);
                System.out.println("pasang        : fallback REPLACE_EXISTING");
            } catch (IOException e2) {
                System.out.println("pasang gagal  : " + e2.getClass().getSimpleName());
                return;
            }
        } catch (IOException e) {
            System.out.println("pasang gagal  : " + e.getClass().getSimpleName());
            return;
        }

        // Verifikasi akhir: baca balik hasil dan hitung barisnya.
        try (BufferedReader pembaca =
                     Files.newBufferedReader(hasil, StandardCharsets.UTF_8)) {
            System.out.println("-- ringkasan.csv --");
            pembaca.lines().forEach(b -> System.out.println("  " + b));
            long jumlah = Files.lines(hasil, StandardCharsets.UTF_8).count();
            System.out.println("verifikasi    : " + jumlah
                    + " baris (1 judul + " + valid.size() + " data)");
        } catch (IOException e) {
            System.out.println("verifikasi gagal: " + e.getClass().getSimpleName());
        }
    }

    // Konfigurasi opsional: hilang atau rusak tidak membatalkan proses.
    static int bacaBatas() {
        try {
            String isi = Files.readString(Path.of("konfigurasi.txt"));
            return Integer.parseInt(isi.trim());
        } catch (IOException | NumberFormatException e) {
            return 60;
        }
    }
}

PulihkanInput.java

Unduh berkas
java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;

public class PulihkanInput {
    public static void main(String[] args) throws IOException {
        Path input = Path.of("pengaturan.txt");
        String isi;

        try {
            isi = Files.readString(input, StandardCharsets.UTF_8);
            System.out.println("sumber        : berkas");
        } catch (NoSuchFileException e) {
            isi = "mode=wajar\n";
            Files.writeString(input, isi, StandardCharsets.UTF_8);
            System.out.println("sumber        : bawaan, berkas dibuat");
        }
        System.out.println("isi           : " + isi.lines().findFirst().orElse(""));
    }
}