CodeGym /Các khóa học /JAVA 25 SELF /DataInputStream, DataOutputStream: làm việc với kiểu dữ l...

DataInputStream, DataOutputStream: làm việc với kiểu dữ liệu nguyên thủy

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

1. Tại sao cần DataInputStream và DataOutputStream?

Khi bạn làm việc với tệp, đôi khi cần lưu không chỉ văn bản mà còn dữ liệu có cấu trúc: số, giá trị boolean, mảng kiểu nguyên thủy. Ví dụ, hãy tưởng tượng bạn viết một trò chơi đơn giản và muốn lưu tiến trình của người dùng: số điểm (int), cấp độ hiện tại (int), thời gian chơi (double), trạng thái chiến thắng (boolean). Tất nhiên có thể ghi ở dạng văn bản:

12345
5
67.5
true

Nhưng điều này bất tiện và không an toàn: cần phân tích (parse) chuỗi, định dạng có thể “bị lệch”, và các số chiếm nhiều chỗ hơn.

Ý tưởng là ghi dữ liệu vào tệp ở dạng “thô” (nhị phân), không chuyển đổi sang văn bản. Để làm điều đó trong Java có hai lớp tuyệt vời:

  • DataOutputStream — có thể ghi các kiểu nguyên thủy vào luồng.
  • DataInputStream — có thể đọc các kiểu nguyên thủy từ luồng.

Chúng hoạt động trên các luồng byte thông thường (OutputStream/InputStream). Nói cách khác, đây là những “lớp bọc” biết không chỉ ghi byte mà còn biết cách từ các byte đó lắp thành int, double, boolean và thậm chí cả String.

Hoạt động như thế nào?

Một FileOutputStream thông thường có thể hình dung như một băng chuyền, nơi bạn tự tay đặt các byte từng cái một. Nếu bạn muốn ghi một số nguyên hoặc một chuỗi, bạn phải tự theo dõi mỗi phần tử chiếm bao nhiêu byte.

DataOutputStream giúp cuộc sống dễ dàng hơn: nó hoạt động như một robot trên băng chuyền đó. Bạn nói với nó “ghi số” hoặc “ghi chuỗi”, và nó tự đóng gói dữ liệu thành đúng số byte rồi gửi xuống đĩa. Ở đầu bên kia băng chuyền có một robot tương tự — DataInputStream — biết lắp các byte này trở lại thành các đối tượng ban đầu.

Vì sao tiện lợi? Vì bạn không cần nghĩ về số byte cho int, double hay boolean. Dữ liệu được lưu gọn, đọc/ghi nhanh, và không có rủi ro lỗi phân tích hay vấn đề về định dạng.

2. Ví dụ: Ghi và đọc các kiểu nguyên thủy

Giả sử chúng ta muốn lưu kết quả của (giả định) trò chơi: tên người chơi (String), số điểm (int), thời gian kỷ lục (double), người chơi có thắng hay không (boolean).

Ghi dữ liệu vào tệp

import java.io.*;

public class SaveGameData {
    public static void main(String[] args) {
        String fileName = "savegame.bin";
        String playerName = "Alice";
        int score = 12345;
        double recordTime = 67.5;
        boolean isWinner = true;

        try (DataOutputStream dos = new DataOutputStream(
                new FileOutputStream(fileName))) {
            dos.writeUTF(playerName);    // Ghi chuỗi (UTF-8)
            dos.writeInt(score);         // Ghi int (4 byte)
            dos.writeDouble(recordTime); // Ghi double (8 byte)
            dos.writeBoolean(isWinner);  // Ghi boolean (1 byte)
            System.out.println("Dữ liệu đã được ghi vào tệp thành công!");
        } catch (IOException e) {
            System.out.println("Lỗi ghi: " + e.getMessage());
        }
    }
}
  • writeUTF(String) — ghi chuỗi theo định dạng UTF-8 (có độ dài ở đầu).
  • writeInt(int) — ghi 4 byte.
  • writeDouble(double) — ghi 8 byte.
  • writeBoolean(boolean) — ghi 1 byte (1 hoặc 0).
  • Tất cả các phương thức tự động “đóng gói” dữ liệu theo định dạng thích hợp.

Đọc dữ liệu từ tệp

import java.io.*;

public class LoadGameData {
    public static void main(String[] args) {
        String fileName = "savegame.bin";

        try (DataInputStream dis = new DataInputStream(
                new FileInputStream(fileName))) {
            String playerName = dis.readUTF();      // Đọc chuỗi
            int score = dis.readInt();              // Đọc int
            double recordTime = dis.readDouble();   // Đọc double
            boolean isWinner = dis.readBoolean();   // Đọc boolean

            System.out.println("Tên người chơi: " + playerName);
            System.out.println("Điểm: " + score);
            System.out.println("Thời gian: " + recordTime);
            System.out.println("Chiến thắng: " + isWinner);
        } catch (IOException e) {
            System.out.println("Lỗi đọc: " + e.getMessage());
        }
    }
}

Điểm quan trọng:
Thứ tự đọc phải trùng với thứ tự ghi! Nếu trước hết ghi chuỗi, sau đó int, rồi double — thì phải đọc theo đúng thứ tự đó. Nếu không bạn sẽ nhận lỗi hoặc dữ liệu “lộn xộn”.

3. Những kiểu nào được hỗ trợ?

DataOutputStreamDataInputStream hỗ trợ tất cả các kiểu nguyên thủy chính của Java:

Phương thức ghi Phương thức đọc Kiểu dữ liệu Kích thước (byte)
writeBoolean(boolean)
readBoolean()
boolean 1
writeByte(int)
readByte()
byte 1
writeShort(int)
readShort()
short 2
writeChar(int)
readChar()
char 2
writeInt(int)
readInt()
int 4
writeLong(long)
readLong()
long 8
writeFloat(float)
readFloat()
float 4
writeDouble(double)
readDouble()
double 8
writeUTF(String)
readUTF()
String (UTF) thay đổi

Ghi chú:
- Với chuỗi, thường dùng writeUTF/readUTF (ghi độ dài chuỗi và chính các byte ở UTF-8).
- Nếu muốn ghi một mảng, trước hết hãy ghi độ dài của mảng, sau đó là từng phần tử.

4. Ví dụ nâng cao: Lưu mảng kiểu nguyên thủy

Ghi mảng

int[] scores = {100, 200, 300, 400, 500};
try (DataOutputStream dos = new DataOutputStream(
        new FileOutputStream("scores.bin"))) {
    dos.writeInt(scores.length); // Trước hết ghi độ dài mảng
    for (int score : scores) {
        dos.writeInt(score);     // Sau đó từng phần tử
    }
}

Đọc mảng

try (DataInputStream dis = new DataInputStream(
        new FileInputStream("scores.bin"))) {
    int length = dis.readInt();      // Đọc độ dài
    int[] scores = new int[length];
    for (int i = 0; i < length; i++) {
        scores[i] = dis.readInt();   // Đọc các phần tử
    }
    // In mảng
    for (int score : scores) {
        System.out.println(score);
    }
}

Vì sao ghi độ dài trước?
Bởi vì khi đọc chúng ta không biết có bao nhiêu số đã được ghi. Ghi độ dài ngay ở đầu giúp định dạng tệp trở nên tự mô tả.

5. Những lưu ý và đặc điểm quan trọng

Khi nào nên dùng DataInputStream/DataOutputStream?

  • Khi cần lưu/tải dữ liệu có cấu trúc gồm các kiểu nguyên thủy.
  • Để trao đổi dữ liệu nhị phân giữa các chương trình Java (hoặc thậm chí các ngôn ngữ khác, nếu bạn biết định dạng).
  • Khi cần độ gọn nhẹ và tốc độ (ví dụ, log, kết quả tính toán, các mảng số lớn).

Khi không nên dùng:

  • Nếu cần định dạng dễ đọc bởi con người (CSV, JSON, XML) — hãy dùng các định dạng văn bản.
  • Với các đối tượng phức tạp có lồng nhau — nên dùng cơ chế tuần tự hóa qua ObjectOutputStream/ObjectInputStream (chủ đề riêng).

Bộ đệm

DataOutputStreamDataInputStream tự thân không đệm dữ liệu. Nếu bạn muốn tăng hiệu năng khi làm việc với tệp lớn, hãy bọc chúng bằng BufferedOutputStream/BufferedInputStream:

try (DataOutputStream dos = new DataOutputStream(
        new BufferedOutputStream(new FileOutputStream("data.bin")))) {
    // ...
}

Mã hóa chuỗi

Các phương thức writeUTF/readUTF dùng một định dạng đặc biệt: trước hết ghi độ dài chuỗi (tính theo byte), sau đó là nội dung ở UTF-8. Đừng nhầm với việc chỉ ghi một mảng byte!

Ngoại lệ

Các thao tác đọc/ghi có thể ném IOException nếu tệp không truy cập được, bị hỏng hoặc kết thúc sớm. Khi cố đọc nhiều hơn những gì đã ghi, thường sẽ nhận EOFException. Luôn dùng cấu trúc try-with-resources hoặc xử lý ngoại lệ bằng try-catch.

Thứ tự đọc và ghi

Lỗi phổ biến nhất — không khớp thứ tự đọc và ghi. Nếu bạn ghi: int, double, boolean, nhưng lại đọc theo double, int, boolean, bạn sẽ nhận dữ liệu sai hoặc ngoại lệ.

6. Các lỗi thường gặp

Lỗi №1: Sai thứ tự ghi và đọc. Nếu bạn thay đổi thứ tự các phương thức, dữ liệu sẽ được đọc không đúng hoặc ném ngoại lệ. Ví dụ, nếu trước hết bạn ghi chuỗi rồi đến số, nhưng khi đọc lại cố đọc số trước — sẽ nhận lỗi định dạng.

Lỗi №2: Quên ghi độ dài mảng. Nếu bạn ghi mảng kiểu nguyên thủy nhưng không ghi độ dài của nó, khi đọc bạn sẽ không biết cần đọc bao nhiêu phần tử. Điều này dẫn đến lỗi hoặc dữ liệu “thừa” ở cuối.

Lỗi №3: Cố đọc sau khi đã đến cuối tệp. Nếu bạn đọc nhiều dữ liệu hơn số đã ghi, bạn sẽ nhận EOFException (end of file).

Lỗi №4: Dùng DataInputStream/DataOutputStream cho tệp văn bản. Các lớp này không dành cho việc đọc các tệp văn bản thông thường được tạo, ví dụ, trong Notepad. Nếu bạn cố đọc một tệp như vậy bằng readInt() — bạn sẽ nhận dữ liệu vô nghĩa hoặc lỗi.

Lỗi №5: Không đóng luồng. Nếu không dùng try-with-resources hoặc không đóng luồng thủ công, tệp có thể vẫn không khả dụng cho chương trình khác hoặc không được lưu hoàn toàn.

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