DataInputStream / DataOutputStream:二进制数据的读写
你有没有想过这个问题:
一张图片的 EXIF 信息、游戏的存档数据、自定义的网络协议... 这些二进制格式是怎么读写?
用普通的字节流当然可以,但每个 int 都要自己转成 4 个字节,读取时再转回来。太繁琐了。
DataInputStream / DataOutputStream 就是来解决这个问题的——它们以二进制格式直接读写 Java 基本类型。
基本用法
写入基本类型
java
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("data.bin")))) {
dos.writeInt(42); // 4 字节
dos.writeLong(1234567890L); // 8 字节
dos.writeDouble(3.14159); // 8 字节
dos.writeBoolean(true); // 1 字节
dos.writeUTF("Hello"); // 变长:2+N 字节
}读取基本类型
java
try (DataInputStream dis = new DataInputStream(
new BufferedInputStream(
new FileInputStream("data.bin")))) {
int i = dis.readInt(); // 42
long l = dis.readLong(); // 1234567890L
double d = dis.readDouble(); // 3.14159
boolean b = dis.readBoolean(); // true
String utf = dis.readUTF(); // "Hello"
}核心原则:读写顺序和类型必须完全一致。
方法对照表
| 写入方法 | 读取方法 | 数据大小 |
|---|---|---|
writeBoolean(boolean) | readBoolean() | 1 字节 |
writeByte(int) | readByte() | 1 字节 |
writeChar(int) | readChar() | 2 字节 |
writeShort(int) | readShort() | 2 字节 |
writeInt(int) | readInt() | 4 字节 |
writeLong(long) | readLong() | 8 字节 |
writeFloat(float) | readFloat() | 4 字节 |
writeDouble(double) | readDouble() | 8 字节 |
writeUTF(String) | readUTF() | 变长(2+N 字节) |
writeChars(String) | readChar() × N | 2×N 字节 |
writeUTF vs writeChars:字符串格式的秘密
这两个方法都写字符串,但格式完全不同:
java
// writeUTF:带 2 字节长度前缀
dos.writeUTF("Hello");
// 实际写入:[0x00, 0x05, 'H', 'e', 'l', 'l', 'o']
// 前两字节是长度(5),后面是内容
// writeChars:不带长度前缀,直接写 Unicode 字符
dos.writeChars("Hello");
// 实际写入:['H', 'e', 'l', 'l', 'o']
// 每个字符 2 字节,共 10 字节读写字符串必须配套使用:
writeUTF/readUTF— 配套,带长度前缀writeChars/readChar()× N — 配套,不带长度
混用会出错:读 writeUTF 写的字符串用 readChar() 会漏掉长度头,解析全乱。
实战:自定义存档文件格式
假设要保存游戏存档:
存档格式:
[name:UTF][level:int][hp:int][mp:int][exp:long][x:double][y:double][items:int][itemId*4:int]java
class Player {
String name;
int level;
int hp;
int mp;
long exp;
double x, y;
int[] itemIds; // 最多 100 个道具
}
// 保存存档
public static void savePlayer(Player player, String path) throws IOException {
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(path)))) {
dos.writeUTF(player.name);
dos.writeInt(player.level);
dos.writeInt(player.hp);
dos.writeInt(player.mp);
dos.writeLong(player.exp);
dos.writeDouble(player.x);
dos.writeDouble(player.y);
// 写道具列表(最多 100 个)
int count = Math.min(player.itemIds.length, 100);
dos.writeInt(count);
for (int i = 0; i < count; i++) {
dos.writeInt(player.itemIds[i]);
}
}
}
// 加载存档
public static Player loadPlayer(String path) throws IOException {
Player player = new Player();
try (DataInputStream dis = new DataInputStream(
new BufferedInputStream(
new FileInputStream(path)))) {
player.name = dis.readUTF();
player.level = dis.readInt();
player.hp = dis.readInt();
player.mp = dis.readInt();
player.exp = dis.readLong();
player.x = dis.readDouble();
player.y = dis.readDouble();
int itemCount = dis.readInt();
player.itemIds = new int[itemCount];
for (int i = 0; i < itemCount; i++) {
player.itemIds[i] = dis.readInt();
}
}
return player;
}大端序与小端序
DataOutputStream 使用大端序(Big Endian):高位字节在前。
java
// 写入 int 42(4 字节,大端序)
dos.writeInt(42);
// 十六进制:00 00 00 2A
// 高位在前
// 小端序需要自己处理
public static void writeIntLittleEndian(DataOutputStream dos, int value)
throws IOException {
dos.writeByte(value & 0xFF);
dos.writeByte((value >> 8) & 0xFF);
dos.writeByte((value >> 16) & 0xFF);
dos.writeByte((value >> 24) & 0xFF);
}常见错误
读写顺序不一致
java
// ❌ 错误:顺序不一致,读出来全是错的
dos.writeInt(id);
dos.writeUTF(name);
dos.writeDouble(score);
// ❌ 错误:类型不匹配
int id = dis.readLong(); // 读了 8 字节当 4 字节用
String name = dis.readInt(); // 读出的 int 被当字符串解析没有处理 EOF
java
// ❌ 错误:读到文件末尾会抛 EOFException
int value = dis.readInt(); // 如果文件只有 2 字节,会抛异常
// ✅ 正确:文件完整性由写入方保证
// 读取前先确认文件大小,或捕获 EOFException记住:
- 读写顺序必须一致
- 读写类型必须匹配
- 字符串用
writeUTF/readUTF,不要用writeChars/readChar - 永远配合 BufferedInputStream / BufferedOutputStream 使用
