CodeGym /Các khóa học /JAVA 25 SELF /Đọc và ghi tệp nhị phân: InputStream, OutputStream

Đọc và ghi tệp nhị phân: InputStream, OutputStream

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Giới thiệu

Hãy bắt đầu từ điều đơn giản nhất: tệp nhị phân khác gì so với tệp văn bản? Tệp văn bản là loại tệp có thể mở bằng trình soạn thảo đơn giản và thấy chữ cái, chữ số, dấu cách và các ký tự khác. Ví dụ, my_notes.txt hoặc poem.txt.

Tệp nhị phân là tệp không chứa văn bản mà là các byte tùy ý. Đó có thể là ảnh (.jpg, .png), nhạc (.mp3), lưu trữ (.zip), tệp thực thi (.exe), video (.mp4), tệp cơ sở dữ liệu, v.v. Nếu bạn mở loại tệp này bằng trình soạn thảo văn bản, bạn sẽ thấy thứ gì đó như ÿØÿà hoặc cả một bức tường ký tự khó hiểu. Điều đó là bình thường! Máy tính chỉ “hiểu” các byte — đối với nó, văn bản, hình ảnh hay video — chỉ là một dãy byte. Trong tệp văn bản, các byte này có thể được diễn giải thành ký tự; còn trong tệp nhị phân, đó là dữ liệu “thô” không dành cho con người đọc.

Các lớp chính để làm việc với tệp nhị phân

Trong Java, để làm việc với tệp nhị phân ta dùng các luồng byte:

  • InputStream — lớp cơ sở để đọc byte.
  • OutputStream — lớp cơ sở để ghi byte.

Để làm việc với tệp có các hiện thực cụ thể:

  • FileInputStream — đọc byte từ tệp.
  • FileOutputStream — ghi byte vào tệp.

Nếu bạn từng nghe về FileReaderFileWriter, hãy biết rằng chúng làm việc với ký tự và chỉ phù hợp cho văn bản. Với tệp nhị phân, chỉ dùng InputStream/OutputStream và các lớp dẫn xuất của chúng.

2. Đọc tệp nhị phân

Đọc từng byte

Cách đơn giản nhất là đọc tệp từng byte. Rõ ràng, nhưng rất chậm.

try (FileInputStream in = new FileInputStream("image.jpg")) {
    int b;
    while ((b = in.read()) != -1) {
        // b là một số từ 0 đến 255 (byte), -1 biểu thị kết thúc tệp
        // Có thể xử lý byte, ví dụ tính tổng của tất cả các byte
    }
}

Phương thức read() trả về byte tiếp theo dạng int (từ 0 đến 255), còn khi tệp đã hết — trả về -1. Thông thường chỉ đọc từng byte khi cần làm điều gì đó rất đặc thù (ví dụ, phân tích cấu trúc tệp).

Đọc theo khối (có bộ đệm)

Đọc từng byte giống như đi chợ mua từng quả táo. Hiệu quả hơn nhiều là lấy cả túi! Trong Java có phương thức read(byte[] buffer), phương thức này điền mảng bằng các byte từ tệp.

try (FileInputStream in = new FileInputStream("image.jpg")) {
    byte[] buffer = new byte[4096]; // bộ đệm 4 KB
    int bytesRead;
    while ((bytesRead = in.read(buffer)) != -1) {
        // buffer chứa bytesRead byte từ tệp
        // Có thể xử lý các byte này, ví dụ lưu chúng ở nơi khác
    }
}

Phương thức read(buffer) trả về số byte thực sự đã đọc (có thể nhỏ hơn kích thước bộ đệm, đặc biệt ở lần đọc cuối). Cách này nhanh hơn nhiều vì số lần truy cập đĩa ít hơn.

Ví dụ: sao chép tệp

Hãy viết một chương trình đơn giản sao chép bất kỳ tệp nhị phân nào (ví dụ, ảnh) từ nơi này sang nơi khác.

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class BinaryCopyExample {
    public static void main(String[] args) {
        String source = "cat.jpg";
        String dest = "cat_copy.jpg";

        try (FileInputStream in = new FileInputStream(source);
             FileOutputStream out = new FileOutputStream(dest)) {

            byte[] buffer = new byte[8192]; // 8 KB — kích thước tối ưu cho đa số tác vụ
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
            System.out.println("Sao chép hoàn tất!");
        } catch (IOException e) {
            System.out.println("Lỗi khi sao chép: " + e.getMessage());
        }
    }
}

Rất đơn giản: đọc các khối từ tệp nguồn và ghi ngay vào tệp mới. Cách này hoạt động với mọi loại tệp: ảnh, lưu trữ, video.

3. Ghi tệp nhị phân

Ghi mảng byte

Nếu bạn có một mảng byte (ví dụ nhận được từ mạng hoặc sinh ra trong chương trình), bạn có thể ghi nó vào tệp như sau:

byte[] data = new byte[] {1, 2, 3, 4, 5}; // ví dụ mảng

try (FileOutputStream out = new FileOutputStream("data.bin")) {
    out.write(data); // ghi toàn bộ mảng vào tệp
}

Phương thức write(byte[]) ghi tất cả các byte trong mảng. Cũng có thể ghi chỉ một phần mảng: out.write(data, offset, length).

Ghi tệp theo từng phần (ví dụ khi sao chép)

Tương tự khi đọc, thường sử dụng bộ đệm:

try (FileInputStream in = new FileInputStream("source.bin");
     FileOutputStream out = new FileOutputStream("dest.bin")) {

    byte[] buffer = new byte[4096];
    int bytesRead;
    while ((bytesRead = in.read(buffer)) != -1) {
        out.write(buffer, 0, bytesRead);
    }
}

Ở đây, mọi thứ đọc từ tệp đều được ghi ngay sang tệp khác. Mẫu mã như vậy thường gặp trong các chương trình nén, trình tải xuống, bộ xử lý ảnh, v.v.

4. Những lưu ý hữu ích

Vì sao không nên dùng Reader/Writer cho tệp nhị phân?

ReaderWriter làm việc với ký tự (char), không phải byte. Chúng tự động chuyển đổi byte thành ký tự theo mã hóa (ví dụ UTF-8). Điều này thuận tiện cho văn bản, nhưng với tệp nhị phân thì cực kỳ nguy hiểm!

Nếu bạn thử ghi một bức ảnh bằng FileWriter, bạn sẽ nhận được tệp hỏng, không thể mở. Hãy ghi nhớ: với mọi tệp không phải văn bản, chỉ dùng InputStream/OutputStream !

Những khác biệt và lưu ý quan trọng khi làm việc với tệp nhị phân

  • Kích thước bộ đệm: Bộ đệm quá nhỏ sẽ làm chậm (quá nhiều lần truy cập đĩa), quá lớn sẽ chiếm thêm bộ nhớ. 416 KB — thường là tối ưu.
  • Xử lý lỗi: Luôn xử lý IOException — tệp có thể không tồn tại, bị khóa hoặc hết dung lượng đĩa.
  • Đóng luồng: Sử dụng try-with-resources — điều này đảm bảo đóng tệp ngay cả khi có lỗi.
  • Ghi đè tệp: Nếu bạn mở tệp qua new FileOutputStream("file.bin"), tệp sẽ bị ghi đè. Để ghi nối vào cuối, dùng constructor với tham số append = true.
  • Quyền truy cập: Nếu chương trình không thể mở tệp, hãy kiểm tra quyền đọc/ghi.
  • readAllBytes(): Cho phép đọc toàn bộ tệp vào mảng byte chỉ bằng một lệnh. Với tệp lớn — đừng dùng, để tránh “ăn” hết bộ nhớ!

5. Các lỗi điển hình khi làm việc với tệp nhị phân

Lỗi №1: Dùng FileReader/FileWriter cho tệp nhị phân. Điều này sẽ làm hỏng dữ liệu, vì các lớp này chuyển đổi byte sang ký tự và ngược lại, điều vốn là thảm họa đối với ảnh, lưu trữ, v.v.

Lỗi №2: Bỏ qua giá trị trả về của read(). Phương thức read(byte[]) có thể đọc ít byte hơn bạn yêu cầu, đặc biệt ở khối cuối. Luôn dùng giá trị trả về để biết bao nhiêu byte thực sự đã được xử lý.

Lỗi №3: Quên đóng luồng. Nếu không đóng tệp, tệp có thể bị khóa và dữ liệu có thể chưa được ghi xong (đặc biệt khi ghi!). Hãy dùng try-with-resources.

Lỗi №4: Cố đọc toàn bộ tệp vào bộ nhớ mà không xét đến kích thước. Với tệp lớn, điều này dẫn tới OutOfMemoryError. Hãy dùng bộ đệm và đọc theo từng phần.

Lỗi №5: Không xử lý ngoại lệ. Làm việc với tệp luôn có thể phát sinh lỗi: tệp không tìm thấy, thiếu quyền, đầy đĩa. Đừng quên xử lý IOException.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION