Skip to content

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() × N2×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 使用

基于 VitePress 构建