Stream java.io mengalir satu arah dengan tangan terbuka: setiap read() menyerahkan byte langsung ke array kita. Channel NIO bekerja dengan kontrak yang berbeda — channel menukar data hanya melalui ByteBuffer, membaca sekaligus sebanyak yang penampung muat dan menulis dari penampung yang sudah di-flip. Pendekatan ini terlihat lebih seremonial untuk berkas kecil, tetapi bersinar pada I/O besar: satu panggilan read atau write memindahkan ribuan byte, channel mengingat posisinya sendiri, dan method transferTo bahkan dapat memindahkan data antar berkas tanpa byte tersebut pernah masuk ke ruang memori Java. Pada Chapter ini kita menyusun dua channel berkas, menyalin data lewat buffer, lalu mencicipi transfer langsung antar channel.

Channel versus Stream #

Perbedaan mendasarnya bisa dirangkum empat poin. Pertama, FileChannel bersifat dua arah — satu channel bisa membaca dan menulis — sedangkan stream satu arah. Kedua, channel berorientasi buffer: semua transfer melewati ByteBuffer, sehingga siklus put-flip-get dari Chapter sebelumnya menjadi nyata di sini. Ketiga, channel menjaga position internal yang bisa dibaca dan disetel, memudahkan akses acak ke tengah berkas. Keempat, channel dirancang untuk ekosistem non-blocking: channel jaringan seperti SocketChannel dapat dimasukkan ke mode non-blocking dan Selector, sehingga satu thread melayani banyak koneksi — fitur yang tidak dimiliki stream. Untuk berkas, FileChannel selalu blocking, tetapi model buffer dan transfer-nya tetap efisien.

Berkas pendamping kutipan.txt (25 byte) menjadi sumber latihan kita. Program berikut menyalinnya ke salinan.txt memakai penampung 4 byte agar putaran siklusnya terlihat:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

public class SalinanChannel {
    public static void main(String[] args) throws IOException {
        Path sumber = Path.of("kutipan.txt");
        Path tujuan = Path.of("salinan.txt");
        long total = 0;

        try (FileChannel masuk = FileChannel.open(sumber, StandardOpenOption.READ);
             FileChannel keluar = FileChannel.open(tujuan,
                     StandardOpenOption.CREATE, StandardOpenOption.WRITE,
                     StandardOpenOption.TRUNCATE_EXISTING)) {
            ByteBuffer penampung = ByteBuffer.allocate(4);
            while (masuk.read(penampung) != -1) {
                penampung.flip();
                total += keluar.write(penampung);
                penampung.clear();
            }
        }

        System.out.println("total byte  : " + total);
        System.out.println("ukuran sama : "
                + (Files.size(sumber) == Files.size(tujuan)));
        System.out.println("isi sama    : "
                + Files.readString(tujuan, StandardCharsets.UTF_8)
                        .equals(Files.readString(sumber, StandardCharsets.UTF_8)));
    }
}

Output yang Dihasilkan:

text
total byte  : 25
ukuran sama : true
isi sama    : true

Loop intinya adalah tarian empat langkah yang berulang:

  1. Baris 11-15 (FileChannel.open dengan StandardOpenOption): channel dibuka tanpa lewat stream — opsi READ, CREATE, WRITE, dan TRUNCATE_EXISTING menyatakan niat secara eksplisit; kombinasi ini setara dengan new FileOutputStream yang menimpa berkas lama.
  2. Baris 20 (masuk.read(penampung)): channel mengisi penampung dari position saat ini sebanyak yang muat, lalu menggeser position buffer; nilai kembalian -1 menandai akhir berkas. Penampung 4 byte membuat berkas 25 byte tersalin dalam tujuh putaran — dua puluh empat byte penuh ditambah satu byte terakhir.
  3. Baris 21 (flip lalu write): flip() memindahkan buffer ke mode baca sehingga write mengirim tepat byte yang barusan terisi; nilai kembalian write adalah jumlah byte yang benar-benar tercatat, diakumulasi ke total.
  4. Baris 22 (clear): mengembalikan buffer ke mode tulis untuk putaran berikutnya. Dua verifikasi penutup membandingkan ukuran dan isi — keduanya cocok, bukti siklus yang seimbang.

Transfer Antar Channel dan newByteChannel #

Mengapa harus repot menyalin lewat buffer bila kernel bisa melakukannya sendiri? transferTo(posisiAwal, jumlah, channelTujuan) meminta sistem operalian memindahkan blok data antar channel secara langsung — pada Linux mekanisme sendfile bahkan bisa berjalan tanpa menyalin byte ke ruang pengguna Java (zero-copy), yang membuat method ini sangat efisien untuk menyalin berkas besar. Satu panggilan mengembalikan jumlah byte yang dipindahkan, dan channel sumber tidak mengubah position miliknya — transfer bekerja pada offset eksplisit, bukan pada kursor.

Cara kedua mendapatkan channel adalah Files.newByteChannel, yang mengembalikan SeekableByteChannel — interface yang diimplementasikan FileChannel — berguna saat kita menulis kode generik terhadap sumber yang bisa dicari posisinya:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

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

        try (FileChannel masuk = FileChannel.open(sumber, StandardOpenOption.READ);
             FileChannel keluar = FileChannel.open(tujuan,
                     StandardOpenOption.CREATE, StandardOpenOption.WRITE,
                     StandardOpenOption.TRUNCATE_EXISTING)) {
            long ukuran = masuk.size();
            long dipindah = masuk.transferTo(0, ukuran, keluar);
            System.out.println("ukuran sumber : " + ukuran);
            System.out.println("dipindah      : " + dipindah);
            System.out.println("posisi sumber : " + masuk.position());
        }

        // Files.newByteChannel menghasilkan SeekableByteChannel.
        try (SeekableByteChannel saluran = Files.newByteChannel(sumber)) {
            ByteBuffer penampung = ByteBuffer.allocate(4);
            saluran.read(penampung);
            penampung.flip();
            byte[] sebagian = new byte[penampung.remaining()];
            penampung.get(sebagian);
            System.out.println("4 byte awal   : "
                    + new String(sebagian, StandardCharsets.UTF_8));
            System.out.println("posisi kanal  : " + saluran.position());
        }
    }
}

Output yang Dihasilkan:

text
ukuran sumber : 25
dipindah      : 25
posisi sumber : 0
4 byte awal   : Bela
posisi kanal  : 4

Keluaran program mengonfirmasi tiga sifat channel:

  1. transferTo(0, ukuran, keluar): seluruh 25 byte berpindah dalam satu panggilan — tanpa buffer yang kita kelola, tanpa perulangan, dan tanpa byte singgah di heap Java.
  2. masuk.position() bernilai 0: transfer bekerja pada offset yang diberikan argumen, bukan pada kursor channel; kursor tidak berpindah sedikit pun. Bandingkan dengan read(buffer) pada blok kedua yang menggeser position channel ke 4 setelah membaca empat byte.
  3. Files.newByteChannel(sumber): membuka channel hanya-baca (tanpa opsi tambahan) dan mengembalikannya sebagai SeekableByteChannel; empat byte pertama Bela terbaca, dan position() pelan-pelan melaporkan 4 — channel sungguhan mengingat tempat berhentinya.

Kita sudah bisa bergerak maju-mundur di dalam isi berkas. Tetapi metadata berkas — ukuran, jenis, stempel waktu — hidup di tempat lain: pada inode dan tabel atribut sistem berkas. Chapter berikutnya memperkenalkan pembacaan atribut lewat Files.getAttribute dan BasicFileAttributes, serta cara mengubahnya secara terprogram.

Referensi #

Berkas Sumber

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

SalinanChannel.java

Unduh berkas
java
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

public class SalinanChannel {
    public static void main(String[] args) throws IOException {
        Path sumber = Path.of("kutipan.txt");
        Path tujuan = Path.of("salinan.txt");
        long total = 0;

        try (FileChannel masuk = FileChannel.open(sumber, StandardOpenOption.READ);
             FileChannel keluar = FileChannel.open(tujuan,
                     StandardOpenOption.CREATE, StandardOpenOption.WRITE,
                     StandardOpenOption.TRUNCATE_EXISTING)) {
            ByteBuffer penampung = ByteBuffer.allocate(4);
            while (masuk.read(penampung) != -1) {
                penampung.flip();
                total += keluar.write(penampung);
                penampung.clear();
            }
        }

        System.out.println("total byte  : " + total);
        System.out.println("ukuran sama : "
                + (Files.size(sumber) == Files.size(tujuan)));
        System.out.println("isi sama    : "
                + Files.readString(tujuan, StandardCharsets.UTF_8)
                        .equals(Files.readString(sumber, StandardCharsets.UTF_8)));
    }
}

TransferChannel.java

Unduh berkas
java
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

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

        try (FileChannel masuk = FileChannel.open(sumber, StandardOpenOption.READ);
             FileChannel keluar = FileChannel.open(tujuan,
                     StandardOpenOption.CREATE, StandardOpenOption.WRITE,
                     StandardOpenOption.TRUNCATE_EXISTING)) {
            long ukuran = masuk.size();
            long dipindah = masuk.transferTo(0, ukuran, keluar);
            System.out.println("ukuran sumber : " + ukuran);
            System.out.println("dipindah      : " + dipindah);
            System.out.println("posisi sumber : " + masuk.position());
        }

        // Files.newByteChannel menghasilkan SeekableByteChannel.
        try (SeekableByteChannel saluran = Files.newByteChannel(sumber)) {
            ByteBuffer penampung = ByteBuffer.allocate(4);
            saluran.read(penampung);
            penampung.flip();
            byte[] sebagian = new byte[penampung.remaining()];
            penampung.get(sebagian);
            System.out.println("4 byte awal   : "
                    + new String(sebagian, StandardCharsets.UTF_8));
            System.out.println("posisi kanal  : " + saluran.position());
        }
    }
}