Apa pun bentuk datanya — teks, gambar, musik, atau berkas ZIP — di tingkat paling bawah disk semua tersimpan sebagai deretan byte. Keluarga byte stream di java.io bekerja tepat pada tingkat itu: InputStream membaca satu byte demi satu byte dari sumber, dan OutputStream menulis byte demi byte ke tujuan. Menguasai lapisan ini memberi kita fondasi untuk memahami semua API I/O lainnya, karena class berbasis karakter dan buffering yang akan kita temui nanti sesungguhnya hanya pembungkus di atas dua abstract class ini. Pada Chapter ini kita membaca dan menulis byte mentah dengan FileInputStream serta FileOutputStream.

Mesin di Balik Read dan Write #

Kedua stream berkas menempel langsung pada file descriptor yang dibuka sistem operasi. Method read() tanpa argumen mengembalikan int — bukan byte — karena JVM memakai nilai -1 sebagai penanda end-of-stream: saat data habis, read() mengembalikan -1, sedangkan byte valid bernilai 0 sampai 255 tak tertukar dengannya. Nilai int yang dikembalikan juga alasan munculnya idiom & 0xFF ketika kita ingin melihat byte mentahnya sebagai angka tak bertanda.

Membaca satu byte per panggilan berarti satu kali perjalanan mahal ke sistem operasi untuk setiap byte. Versi berkinerja jauh lebih baik memakai read(byte[]): sekali panggil mengisi penampung sebanyak mungkin byte, lalu kita memproses penampung itu di memori. Perhatikan bahwa read(byte[]) boleh mengembalikan kurang dari panjang penampung — nilai kembalian dibaca wajib dipakai saat menulis agar byte basi di ujung penampung tidak ikut tersalin.

Program berikut menyalin berkas pendamping catatan.txt (15 byte) ke salinan.txt memakai penampung 4 byte supaya pola putarannya terlihat:

java SalinByte.java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class SalinByte {
    public static void main(String[] args) throws IOException {
        Path sumber = Path.of("catatan.txt");
        Path tujuan = Path.of("salinan.txt");

        long total = 0;
        try (FileInputStream masuk = new FileInputStream("catatan.txt");
             FileOutputStream keluar = new FileOutputStream("salinan.txt")) {
            byte[] penampung = new byte[4];
            int dibaca;
            while ((dibaca = masuk.read(penampung)) != -1) {
                keluar.write(penampung, 0, dibaca);
                total += dibaca;
            }
        }

        System.out.println("total byte   : " + total);
        System.out.println("ukuran sama  : " + (Files.size(sumber) == Files.size(tujuan)));

        try (FileInputStream masuk = new FileInputStream("salinan.txt")) {
            System.out.print("byte pertama : ");
            for (int i = 0; i < 4; i++) {
                System.out.print((masuk.read() & 0xFF) + " ");
            }
            System.out.println();
        }
    }
}

Output yang Dihasilkan:

text
total byte   : 15
ukuran sama  : true
byte pertama : 66 117 100 105 

Mari kita bedah pola inti pada program di atas:

  1. Baris 11-12 (try-with-resources dua stream): kedua stream dideklarasikan di header try sehingga close() otomatis terpanggil berurutan untuk keduanya saat blok berakhir — FileOutputStream ditutup lebih dulu agar semua byte terdorong keluar.
  2. Baris 13 (byte[] penampung = new byte[4]): penampung sengaja kecil agar loop berputar beberapa kali; pada kode produksi ukuran lazim 1024 sampai 8192.
  3. Baris 15 (while ((dibaca = masuk.read(penampung)) != -1): satu baris yang sekaligus membaca, menyimpan jumlah byte nyata, dan menguji akhir berkas.
  4. Baris 16 (keluar.write(penampung, 0, dibaca)): menulis hanya dibaca byte dari indeks 0. Pada putaran terakhir penampung hanya terisi sebagian; tanpa batas dibaca, byte sisa dari putaran sebelumnya ikut tersalin dan hasilnya rusak.
  5. Baris 27 (masuk.read() & 0xFF): read() tunggal mengembalikan int 0-255 untuk byte valid, sehingga & 0xFF di sini menegaskan rentang tak bertanda secara eksplisit.

Biner versus Teks #

Teori karakter hanyalah interpretasi: Budi\n di catatan.txt tersimpan sebagai angka 66 117 100 105 10. Membacanya lewat byte stream memperlihatkan angka-angka itu apa adanya, tanpa dekoder karakter apa pun. Format biner buatan sendiri biasanya menyisipkan metadata — misalnya satu byte panjang sebelum isi — sehingga pembaca tahu berapa byte yang harus dikonsumsi berikutnya.

Program kedua membuktikan keduanya: membaca teks sebagai byte mentah, lalu menulis dan memulihkan format biner panjang-plus-isi:

java BacaByte.java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class BacaByte {
    public static void main(String[] args) throws IOException {
        System.out.print("byte catatan.txt : ");
        try (FileInputStream masuk = new FileInputStream("catatan.txt")) {
            int b;
            while ((b = masuk.read()) != -1) {
                System.out.print(b + " ");
            }
        }
        System.out.println();

        // Format biner sederhana: satu byte panjang, lalu byte isi.
        String pesan = "Budi";
        try (FileOutputStream keluar = new FileOutputStream("pesan.bin")) {
            byte[] isi = pesan.getBytes(StandardCharsets.UTF_8);
            keluar.write(isi.length);
            keluar.write(isi);
        }

        try (FileInputStream masuk = new FileInputStream("pesan.bin")) {
            int panjang = masuk.read();
            byte[] isi = masuk.readNBytes(panjang);
            String hasil = new String(isi, StandardCharsets.UTF_8);
            System.out.println("panjang tertulis : " + panjang);
            System.out.println("isi pulih        : " + hasil);
        }
    }
}

Output yang Dihasilkan:

text
byte catatan.txt : 66 117 100 105 10 83 105 116 105 10 68 101 119 105 10 
panjang tertulis : 4
isi pulih        : Budi

Keluaran pertama menunjukkan pemetaan huruf ke angka: B adalah 66, u adalah 117, hingga tanda baris baru bernilai 10 — teks memang sekadar byte yang kita sepakati cara membacanya. Segmen penulisan memakai String.getBytes(StandardCharsets.UTF_8) untuk mengubah teks menjadi byte sebelum masuk ke stream biner, menyisipkan isi.length sebagai byte panjang di depan. Segmen pembacaan memanggil read() sekali untuk panjang, lalu readNBytes(panjang) mengambil tepat sebanyak itu; readNBytes memblokir sampai jumlah yang diminta terpenuhi atau stream berakhir. Baris terakhir menyusun ulang String dari byte dengan charset yang sama — bukti data utuh melalui perjalanan biner.

Kita sudah bisa memindahkan byte mentah secara andal. Namun begitu isi berkas berupa teks — dan mayoritas data dunia software adalah teks — kita membutuhkan lapisan yang memahami karakter, charset, dan baris. Chapter berikutnya memperkenalkan character stream Reader dan Writer.

Referensi #

Berkas Sumber

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

BacaByte.java

Unduh berkas
java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class BacaByte {
    public static void main(String[] args) throws IOException {
        System.out.print("byte catatan.txt : ");
        try (FileInputStream masuk = new FileInputStream("catatan.txt")) {
            int b;
            while ((b = masuk.read()) != -1) {
                System.out.print(b + " ");
            }
        }
        System.out.println();

        // Format biner sederhana: satu byte panjang, lalu byte isi.
        String pesan = "Budi";
        try (FileOutputStream keluar = new FileOutputStream("pesan.bin")) {
            byte[] isi = pesan.getBytes(StandardCharsets.UTF_8);
            keluar.write(isi.length);
            keluar.write(isi);
        }

        try (FileInputStream masuk = new FileInputStream("pesan.bin")) {
            int panjang = masuk.read();
            byte[] isi = masuk.readNBytes(panjang);
            String hasil = new String(isi, StandardCharsets.UTF_8);
            System.out.println("panjang tertulis : " + panjang);
            System.out.println("isi pulih        : " + hasil);
        }
    }
}

SalinByte.java

Unduh berkas
java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class SalinByte {
    public static void main(String[] args) throws IOException {
        Path sumber = Path.of("catatan.txt");
        Path tujuan = Path.of("salinan.txt");

        long total = 0;
        try (FileInputStream masuk = new FileInputStream("catatan.txt");
             FileOutputStream keluar = new FileOutputStream("salinan.txt")) {
            byte[] penampung = new byte[4];
            int dibaca;
            while ((dibaca = masuk.read(penampung)) != -1) {
                keluar.write(penampung, 0, dibaca);
                total += dibaca;
            }
        }

        System.out.println("total byte   : " + total);
        System.out.println("ukuran sama  : " + (Files.size(sumber) == Files.size(tujuan)));

        try (FileInputStream masuk = new FileInputStream("salinan.txt")) {
            System.out.print("byte pertama : ");
            for (int i = 0; i < 4; i++) {
                System.out.print((masuk.read() & 0xFF) + " ");
            }
            System.out.println();
        }
    }
}