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