Byte stream memindahkan angka; manusia membaca huruf. Ketika berkas yang kita olah berupa teks — konfigurasi, CSV, catatan — lapisan character stream Reader dan Writer mengambil alih tugas menerjemahkan byte menjadi karakter dan sebaliknya menggunakan charset. Terjemahan ini bukan detail sepele: satu karakter bisa terdiri dari satu sampai empat byte UTF-8, dan kesalahan penerjemahan menghasilkan teks yang tampak asing, bukan error yang jelas. Pada Chapter ini kita membaca serta menulis teks dengan FileReader, FileWriter, jembatan InputStreamReader/OutputStreamWriter, dan PrintWriter — semuanya dengan charset eksplisit.

Reader, Writer, dan Charset di Baliknya #

Reader adalah mitra InputStream: method read() mengembalikan int berisi satu karakter (0 sampai 65535, karena karakter Java adalah UTF-16), atau -1 saat akhir stream. Untuk berkas, FileReader membuka byte stream lalu membungkusnya dengan dekoder charset; Writer bekerja arah sebaliknya dengan enkoder. Charset menentukan pemetaan byte-ke-karakter — UTF-8 mengenkode é sebagai dua byte C3 A9, sementara ISO-8859-1 memakai satu byte E9.

Berkas pendamping pesan.txt berisi teks dua baris dengan satu huruf é. Program berikut membacanya dua kali dengan dua charset berbeda untuk melihat dampaknya:

import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

public class BacaKarakter {
    public static void main(String[] args) throws IOException {
        Path berkas = Path.of("pesan.txt");
        System.out.println("ukuran byte     : " + Files.size(berkas));

        StringBuilder teks = new StringBuilder();
        try (FileReader pembaca = new FileReader("pesan.txt", StandardCharsets.UTF_8)) {
            char[] penampung = new char[8];
            int dibaca;
            while ((dibaca = pembaca.read(penampung)) != -1) {
                teks.append(penampung, 0, dibaca);
            }
        }
        System.out.println("jumlah karakter : " + teks.length());
        System.out.println("baris terakhir  : " + teks.toString().lines().toList().get(1));

        StringBuilder salah = new StringBuilder();
        try (FileReader pembaca = new FileReader("pesan.txt", StandardCharsets.ISO_8859_1)) {
            char[] penampung = new char[8];
            int dibaca;
            while ((dibaca = pembaca.read(penampung)) != -1) {
                salah.append(penampung, 0, dibaca);
            }
        }
        System.out.println("dibaca latin-1  : " + salah.toString().lines().toList().get(1));
    }
}

Output yang Dihasilkan:

text
ukuran byte     : 24
jumlah karakter : 23
baris terakhir  : Café Java.
dibaca latin-1  : Café Java.

Tiga angka pada keluaran menyimpan pelajaran penting:

  1. Baris 12 (Files.size): berkas berukuran 24 byte, padahal isinya hanya 23 karakter — huruf é memakan dua byte dalam UTF-8, bukti bahwa byte dan karakter bukan satuan yang sama.
  2. Baris 14-24 (loop read(char[])): pola penampung mirip byte stream, tetapi yang terisi adalah char, dan teks.length() bernilai 23 karena é dihitung satu karakter.
  3. Baris 27-36 (ISO_8859_1): dua byte C3 A9 yang semestinya menjadi satu é diterjemahkan sebagai dua karakter terpisah à dan ©, menghasilkan Café Java. — teks tampak rusak tanpa satu pun exception. Kesalahan charset adalah kesalahan diam-diam; selalu nyatakan charset saat membuka berkas teks.

Dua class jembatan menjadi fondasi seluruh API teks: InputStreamReader mengubah InputStream byte menjadi Reader karakter, dan OutputStreamWriter mengubah Writer menjadi OutputStream byte. FileReader dan FileWriter sesungguhnya hanya jembatan tersebut yang sudah terikat ke berkas. Dengan jembatan, kita bebas mengganti sumber byte — berkas, jaringan, memori — tanpa mengubah sisa kode, dan menyusun dekorator seperti PrintWriter di atasnya untuk mendapatkan println serta printf.

Program berikut menulis surat pendek melalui tiga jalur penulis berbeda, lalu membacanya kembali:

java TulisTeks.java
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.List;

public class TulisTeks {
    public static void main(String[] args) throws IOException {
        // FileWriter dengan charset eksplisit.
        try (FileWriter penulis = new FileWriter("surat.txt", StandardCharsets.UTF_8)) {
            penulis.write("Halo Budi,\n");
            penulis.write("sampai jumpa di kelas berikutnya.\n");
        }

        // Jembatan: OutputStreamWriter membungkus FileOutputStream.
        try (OutputStreamWriter penulis = new OutputStreamWriter(
                new FileOutputStream("surat.txt", true), StandardCharsets.UTF_8)) {
            penulis.write("— Siti\n");
        }

        // PrintWriter di atas jembatan yang sama.
        try (PrintWriter penulis = new PrintWriter(new OutputStreamWriter(
                new FileOutputStream("surat.txt", true), StandardCharsets.UTF_8))) {
            penulis.println("Baris dari PrintWriter.");
            penulis.printf("Total %d baris selesai.%n", 5);
        }

        // Baca balik lewat jembatan pembaca.
        try (InputStreamReader pembaca = new InputStreamReader(
                new FileInputStream("surat.txt"), StandardCharsets.UTF_8)) {
            List<String> baris = new BufferedReader(pembaca).lines().toList();
            System.out.println("jumlah baris   : " + baris.size());
            System.out.println("tanda tangan   : " + baris.get(2));
            System.out.println("baris terakhir : " + baris.get(baris.size() - 1));
        }
    }
}

Output yang Dihasilkan:

text
jumlah baris   : 5
tanda tangan   : — Siti
baris terakhir : Total 5 baris selesai.

Susunan dekorator pada program tersebut bekerja seperti pipa bertingkat:

  1. Baris 16-20 (FileWriter): constructor dengan charset menulis berkas baru; tanpa argumen append, berkas lama dengan nama sama terpotong kosong sejak awal.
  2. Baris 23-26 (OutputStreamWriter): constructor FileOutputStream kedua argumen bernilai true membuka mode append sehingga baris tanda tangan — Siti menyambung, bukan menimpa. Karakter — (em dash) terkirim sebagai tiga byte UTF-8 dan pulih utuh saat dibaca.
  3. Baris 29-33 (PrintWriter): memberi kemampuan println dan printf di atas jembatan byte-ke-karakter. Perhatikan bahwa PrintWriter sendiri tidak melempar IOException — kesalahan I/O hanya terdeteksi lewat checkError(), sehingga untuk berkas penting lebih baik memakai BufferedWriter yang exception-nya jujur.
  4. Baris 36-42 (InputStreamReader): sisi pembaca yang bercermin — byte dari FileInputStream diterjemahkan kembali menjadi karakter, dibungkus BufferedReader agar bisa dibaca per baris, dan hasilnya terbukti lima baris dengan tanda tangan serta baris penutup utuh.

Perbandingan ringkas dua keluarga stream sebelum kita lanjut:

AspekByte streamCharacter stream
Class dasarInputStream / OutputStreamReader / Writer
Satuan databyte (8-bit)karakter UTF-16
Charsettidak pedulipusat perhatian
Cocok untukgambar, arsip, format binerteks, CSV, JSON, log

Setiap read() dan write() pada contoh tadi menempuh perjalanan ke sistem operasi begitu saja. Untuk berkas besar, perjalanan itu mahal — dan di sanalah buffering masuk. Chapter berikutnya memperkenalkan BufferedReader dan BufferedWriter beserta konsep flush yang menyertainya.

Referensi #

Berkas Sumber

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

BacaKarakter.java

Unduh berkas
java
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

public class BacaKarakter {
    public static void main(String[] args) throws IOException {
        Path berkas = Path.of("pesan.txt");
        System.out.println("ukuran byte     : " + Files.size(berkas));

        StringBuilder teks = new StringBuilder();
        try (FileReader pembaca = new FileReader("pesan.txt", StandardCharsets.UTF_8)) {
            char[] penampung = new char[8];
            int dibaca;
            while ((dibaca = pembaca.read(penampung)) != -1) {
                teks.append(penampung, 0, dibaca);
            }
        }
        System.out.println("jumlah karakter : " + teks.length());
        System.out.println("baris terakhir  : " + teks.toString().lines().toList().get(1));

        StringBuilder salah = new StringBuilder();
        try (FileReader pembaca = new FileReader("pesan.txt", StandardCharsets.ISO_8859_1)) {
            char[] penampung = new char[8];
            int dibaca;
            while ((dibaca = pembaca.read(penampung)) != -1) {
                salah.append(penampung, 0, dibaca);
            }
        }
        System.out.println("dibaca latin-1  : " + salah.toString().lines().toList().get(1));
    }
}

TulisTeks.java

Unduh berkas
java
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.List;

public class TulisTeks {
    public static void main(String[] args) throws IOException {
        // FileWriter dengan charset eksplisit.
        try (FileWriter penulis = new FileWriter("surat.txt", StandardCharsets.UTF_8)) {
            penulis.write("Halo Budi,\n");
            penulis.write("sampai jumpa di kelas berikutnya.\n");
        }

        // Jembatan: OutputStreamWriter membungkus FileOutputStream.
        try (OutputStreamWriter penulis = new OutputStreamWriter(
                new FileOutputStream("surat.txt", true), StandardCharsets.UTF_8)) {
            penulis.write("— Siti\n");
        }

        // PrintWriter di atas jembatan yang sama.
        try (PrintWriter penulis = new PrintWriter(new OutputStreamWriter(
                new FileOutputStream("surat.txt", true), StandardCharsets.UTF_8))) {
            penulis.println("Baris dari PrintWriter.");
            penulis.printf("Total %d baris selesai.%n", 5);
        }

        // Baca balik lewat jembatan pembaca.
        try (InputStreamReader pembaca = new InputStreamReader(
                new FileInputStream("surat.txt"), StandardCharsets.UTF_8)) {
            List<String> baris = new BufferedReader(pembaca).lines().toList();
            System.out.println("jumlah baris   : " + baris.size());
            System.out.println("tanda tangan   : " + baris.get(2));
            System.out.println("baris terakhir : " + baris.get(baris.size() - 1));
        }
    }
}