1. 為什麼需要 DataInputStream 和 DataOutputStream?
當你處理檔案時,有時不只是要儲存純文字,而是要儲存結構化資料:數字、布林值、基本型別陣列。例如,你在寫一個小遊戲,想保存使用者進度:分數(int)、目前等級(int)、遊戲時間(double)、是否為勝利者(boolean)。當然,你可以用純文字格式寫成這樣:
12345
5
67.5
true
但這既不方便也不安全:你得自己剖析字串,格式可能會「走樣」,而且數字占用更多空間.
重點在於直接以「原始」(二進位)形式寫入檔案,不做文字轉換。為此,Java 提供了兩個很棒的類別:
- DataOutputStream — 能將基本型別寫入串流。
- DataInputStream — 能從串流讀取基本型別。
它們是建構在一般位元組串流(OutputStream/InputStream)之上的「包裝層」。也就是說,它們不只寫入位元組,還知道如何把這些位元組組合成 int、double、boolean,甚至 String。
它是如何運作的?
一般的 FileOutputStream 可以想像成一條輸送帶,你得手動一個一個放入位元組。若要寫入整數或字串,就得自己追蹤每個元素占用多少位元組。
DataOutputStream 能大幅簡化這件事:它就像輸送帶上的機器人。你對它說「寫入數字」或「寫入字串」,它就會自動把資料打包成正確的位元組數並寫到磁碟。輸送帶的另一端還有一台機器人 — DataInputStream — 會把位元組還原成原本的物件。
為什麼這樣更方便? 因為你不必再去計算 int、double 或 boolean 需要多少位元組。資料儲存更精簡、讀寫速度更快,也不會有剖析錯誤或格式問題。
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 位元組(1 或 0)。
- 所有方法都會自動以正確格式「封裝」資料。
從檔案讀取資料
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. 支援哪些型別?
DataOutputStream 和 DataInputStream 支援所有主要的 Java 基本型別:
| 寫入方法 | 讀取方法 | 資料型別 | 大小(位元組) |
|---|---|---|---|
|
|
boolean | 1 |
|
|
byte | 1 |
|
|
short | 2 |
|
|
char | 2 |
|
|
int | 4 |
|
|
long | 8 |
|
|
float | 4 |
|
|
double | 8 |
|
|
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(另有主題)。
緩衝處理
DataOutputStream 和 DataInputStream 本身不會作緩衝。若要在處理大型檔案時提升效能,請將它們再包一層 BufferedOutputStream/BufferedInputStream:
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream("data.bin")))) {
// ...
}
字串編碼
方法 writeUTF/readUTF 使用一種特殊格式:會先寫入字串長度(以位元組計),再寫入 UTF-8 內容。別把它和單純寫入位元組陣列混淆!
例外狀況
讀寫操作可能會拋出 IOException,例如檔案無法存取、已損壞,或過早結束。當嘗試讀取超過已寫入的資料時,常見的例外是 EOFException。務必使用 try-with-resources 結構,或處理例外 try-catch。
讀寫順序
最常見的錯誤 —— 讀取與寫入的順序不一致。若你寫入的順序是:int、double、boolean,卻以 double、int、boolean 的順序讀取,就會得到錯誤資料或例外。
6. 常見錯誤
錯誤 №1:讀寫順序被打亂。 如果你更動了方法的順序,資料可能被錯誤讀取或直接拋出例外。比如先寫入字串再寫入數字,但讀取時先讀數字 —— 就會得到格式錯誤。
錯誤 №2:忘了寫入陣列長度。 如果寫入的是基本型別陣列卻沒有先寫入長度,讀取時就不知道要讀多少元素。這會導致錯誤,或在結尾出現「多餘」的資料。
錯誤 №3:越過檔案結尾仍嘗試讀取。 如果你讀取的資料多於實際寫入的量,會得到 EOFException(end of file)。
錯誤 №4:用 DataInputStream/DataOutputStream 讀寫純文字檔。 這些類別不是為了讀取一般的純文字檔而設計(例如用記事本建立的檔案)。如果你嘗試用 readInt() 去讀這種檔案 —— 得到的只會是沒有意義的資料或錯誤。
錯誤 №5:忘了關閉串流。 如果不使用 try-with-resources 或不手動關閉串流,檔案可能會無法被其他程式存取,或資料未被完整保存。
GO TO FULL VERSION