CodeGym /課程 /JAVA 25 SELF /DataInputStream、DataOutputStream:處理基本型別

DataInputStream、DataOutputStream:處理基本型別

JAVA 25 SELF
等級 36 , 課堂 3
開放

1. 為什麼需要 DataInputStream 和 DataOutputStream?

當你處理檔案時,有時不只是要儲存純文字,而是要儲存結構化資料:數字、布林值、基本型別陣列。例如,你在寫一個小遊戲,想保存使用者進度:分數(int)、目前等級(int)、遊戲時間(double)、是否為勝利者(boolean)。當然,你可以用純文字格式寫成這樣:

12345
5
67.5
true

但這既不方便也不安全:你得自己剖析字串,格式可能會「走樣」,而且數字占用更多空間.

重點在於直接以「原始」(二進位)形式寫入檔案,不做文字轉換。為此,Java 提供了兩個很棒的類別:

  • DataOutputStream — 能將基本型別寫入串流。
  • DataInputStream — 能從串流讀取基本型別。

它們是建構在一般位元組串流(OutputStream/InputStream)之上的「包裝層」。也就是說,它們不只寫入位元組,還知道如何把這些位元組組合成 intdoubleboolean,甚至 String

它是如何運作的?

一般的 FileOutputStream 可以想像成一條輸送帶,你得手動一個一個放入位元組。若要寫入整數或字串,就得自己追蹤每個元素占用多少位元組。

DataOutputStream 能大幅簡化這件事:它就像輸送帶上的機器人。你對它說「寫入數字」或「寫入字串」,它就會自動把資料打包成正確的位元組數並寫到磁碟。輸送帶的另一端還有一台機器人 — DataInputStream — 會把位元組還原成原本的物件。

為什麼這樣更方便? 因為你不必再去計算 intdoubleboolean 需要多少位元組。資料儲存更精簡、讀寫速度更快,也不會有剖析錯誤或格式問題。

2. 範例:寫入與讀取基本型別

假設我們想保存(假想的)遊戲結果:玩家名稱(String)、分數(int)、最佳時間(double)、是否勝利(boolean)。

將資料寫入檔案

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);    // 寫入字串 (UTF-8)
            dos.writeInt(score);         // 寫入 int (4 位元組)
            dos.writeDouble(recordTime); // 寫入 double (8 位元組)
            dos.writeBoolean(isWinner);  // 寫入 boolean (1 位元組)
            System.out.println("資料已成功寫入檔案!");
        } catch (IOException e) {
            System.out.println("寫入錯誤: " + e.getMessage());
        }
    }
}
  • writeUTF(String) — 以 UTF-8 格式寫入字串(前面會寫入長度)。
  • writeInt(int) — 寫入 4 位元組。
  • writeDouble(double) — 寫入 8 位元組。
  • writeBoolean(boolean) — 寫入 1 位元組(10)。
  • 所有方法都會自動以正確格式「封裝」資料。

從檔案讀取資料

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();      // 讀取字串
            int score = dis.readInt();              // 讀取 int
            double recordTime = dis.readDouble();   // 讀取 double
            boolean isWinner = dis.readBoolean();   // 讀取 boolean

            System.out.println("玩家名稱: " + playerName);
            System.out.println("分數: " + score);
            System.out.println("時間: " + recordTime);
            System.out.println("是否勝利: " + isWinner);
        } catch (IOException e) {
            System.out.println("讀取錯誤: " + e.getMessage());
        }
    }
}

重點:
讀取順序必須與寫入順序一致! 如果先寫入字串,再寫入 int,再寫入 double —— 就必須以相同順序讀取。否則會得到錯誤或「亂碼」資料。

3. 支援哪些型別?

DataOutputStreamDataInputStream 支援所有主要的 Java 基本型別:

寫入方法 讀取方法 資料型別 大小(位元組)
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) 可變

備註:
- 字串通常使用 writeUTF/readUTF(會先寫入字串長度,接著以 UTF-8 寫入位元組)。
- 若要寫入陣列,請先寫入其長度,然後逐一寫入元素。

4. 進階範例:儲存基本型別陣列

寫入陣列

int[] scores = {100, 200, 300, 400, 500};
try (DataOutputStream dos = new DataOutputStream(
        new FileOutputStream("scores.bin"))) {
    dos.writeInt(scores.length); // 先寫入陣列長度
    for (int score : scores) {
        dos.writeInt(score);     // 再寫入每個元素
    }
}

讀取陣列

try (DataInputStream dis = new DataInputStream(
        new FileInputStream("scores.bin"))) {
    int length = dis.readInt();      // 讀取長度
    int[] scores = new int[length];
    for (int i = 0; i < length; i++) {
        scores[i] = dis.readInt();   // 讀取元素
    }
    // 輸出陣列
    for (int score : scores) {
        System.out.println(score);
    }
}

為什麼要先寫入長度?
因為讀取時我們不知道寫入了多少個數字。先把長度寫在開頭,就能讓檔案格式自我描述。

5. 重要細節與注意事項

什麼時候應該使用 DataInputStream/DataOutputStream?

  • 需要保存/載入由基本型別構成的結構化資料時。
  • 在 Java 程式之間(甚至跨語言,只要你知道格式)交換二進位資料時。
  • 當重視緊湊度與速度時(例如日誌、計算結果、大量數值陣列)。

不適用的情況:

  • 若需要人類可讀的格式(CSV、JSON、XML)—— 請改用文字格式。
  • 對於具有巢狀結構的複雜物件 —— 較適合使用序列化 ObjectOutputStream/ObjectInputStream(另有主題)。

緩衝處理

DataOutputStreamDataInputStream 本身不會作緩衝。若要在處理大型檔案時提升效能,請將它們再包一層 BufferedOutputStream/BufferedInputStream

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

字串編碼

方法 writeUTF/readUTF 使用一種特殊格式:會先寫入字串長度(以位元組計),再寫入 UTF-8 內容。別把它和單純寫入位元組陣列混淆!

例外狀況

讀寫操作可能會拋出 IOException,例如檔案無法存取、已損壞,或過早結束。當嘗試讀取超過已寫入的資料時,常見的例外是 EOFException。務必使用 try-with-resources 結構,或處理例外 try-catch

讀寫順序

最常見的錯誤 —— 讀取與寫入的順序不一致。若你寫入的順序是:intdoubleboolean,卻以 doubleintboolean 的順序讀取,就會得到錯誤資料或例外。

6. 常見錯誤

錯誤 №1:讀寫順序被打亂。 如果你更動了方法的順序,資料可能被錯誤讀取或直接拋出例外。比如先寫入字串再寫入數字,但讀取時先讀數字 —— 就會得到格式錯誤。

錯誤 №2:忘了寫入陣列長度。 如果寫入的是基本型別陣列卻沒有先寫入長度,讀取時就不知道要讀多少元素。這會導致錯誤,或在結尾出現「多餘」的資料。

錯誤 №3:越過檔案結尾仍嘗試讀取。 如果你讀取的資料多於實際寫入的量,會得到 EOFException(end of file)。

錯誤 №4:用 DataInputStream/DataOutputStream 讀寫純文字檔。 這些類別不是為了讀取一般的純文字檔而設計(例如用記事本建立的檔案)。如果你嘗試用 readInt() 去讀這種檔案 —— 得到的只會是沒有意義的資料或錯誤。

錯誤 №5:忘了關閉串流。 如果不使用 try-with-resources 或不手動關閉串流,檔案可能會無法被其他程式存取,或資料未被完整保存。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION