← 返回题目列表

DataInputStream/DataOutputStream 是干什么的?怎么读写基本类型和处理字节序?

简单 第 17 / 22 题 更新于 2026/07/28
DataInputStreamDataOutputStream基本类型读写字节序

简化版

**DataInputStream/DataOutputStream 是「能直接读写 Java 基本类型(int、long、double、boolean、UTF 字符串等)」的处理流。**普通字节流(InputStream/OutputStream)只能读写「字节」——你要写一个 int,得自己把它拆成 4 个字节;要读一个 long,得自己把 8 个字节拼回来,很麻烦。DataOutputStream 提供 writeInt/writeLong/writeDouble/writeUTF 等方法,自动帮你把基本类型转成对应的字节序列写出DataInputStream 提供 readInt/readLong 等,自动把字节读回成基本类型关键特性① 它们是「装饰流」,要套在别的流外面(如 new DataOutputStream(new BufferedOutputStream(new FileOutputStream(f))));② 字节序固定是「大端(Big-Endian)」——writeInt 会按「高字节在前」的顺序写,跨语言/跨系统交互时要注意(Java 网络传输的标准字节序就是大端);③ 读写要「对称」——写的时候用 writeInt 写了,读的时候必须用 readInt 按同样的顺序和类型读,否则数据错乱。用途:读写二进制格式的文件、简单的自定义二进制协议。

详细版

DataOutputStream/DataInputStream 的方法

写方法读方法类型字节数
writeIntreadIntint4
writeLongreadLonglong8
writeDoublereadDoubledouble8
writeBooleanreadBooleanboolean1
writeCharreadCharchar2
writeUTFreadUTF字符串(改进 UTF-8)2+N
// 写基本类型到文件
try (DataOutputStream dos = new DataOutputStream(
        new BufferedOutputStream(new FileOutputStream("data.bin")))) {
    dos.writeInt(42);           // 写 4 字节
    dos.writeLong(123456789L);  // 写 8 字节
    dos.writeDouble(3.14);      // 写 8 字节
    dos.writeBoolean(true);     // 写 1 字节
    dos.writeUTF("你好");        // 写 2 字节长度 + UTF-8 字节
}

// 读回来(必须按写入的顺序和类型对称读取!)
try (DataInputStream dis = new DataInputStream(
        new BufferedInputStream(new FileInputStream("data.bin")))) {
    int i = dis.readInt();        // 读 4 字节 → 42
    long l = dis.readLong();      // 读 8 字节 → 123456789
    double d = dis.readDouble();  // 读 8 字节 → 3.14
    boolean b = dis.readBoolean();// 读 1 字节 → true
    String s = dis.readUTF();     // 读长度 + 内容 → "你好"
}

⚠️ DataStream 的第一铁律是「读写必须对称」——数据在字节流里是没有「类型标记」的,writeInt(42) 写出的就是 4 个字节,字节本身不知道自己是个 int。所以读的时候,你必须「知道」这里应该读一个 int,用 readInt() 读 4 字节;如果顺序或类型搞错了(比如写的时候先 writeIntwriteLong,读的时候先 readLong),字节就会被错误地拼接,读出完全错误的值,且通常不会报错(因为字节数够,只是拼错了)。所以用 DataStream 定义二进制格式时,写入顺序/类型 = 读取顺序/类型,必须严格一致——这也是为什么二进制格式往往要写清楚「字段布局文档」。另外 writeUTF 用的是「改进版 UTF-8」(前面加 2 字节长度),和标准 UTF-8 略有不同,跨语言交互时要留意。

完整版教学

一、问题:字节流读写基本类型很麻烦

先理解为什么需要 DataStream

普通字节流只能读写"字节"(byte / byte[]):
  OutputStream.write(int b)      写一个字节
  OutputStream.write(byte[] b)   写一批字节

想写一个 int(4 字节)怎么办?
  int value = 42;
  // 手动把 int 拆成 4 个字节(大端)
  out.write((value >>> 24) & 0xFF);
  out.write((value >>> 16) & 0xFF);
  out.write((value >>> 8) & 0xFF);
  out.write(value & 0xFF);
  → 繁琐、易错

想读一个 int?
  int value = (b1 << 24) | (b2 << 16) | (b3 << 8) | b4;
  → 又要手动拼

痛点:基本类型 ↔ 字节 的转换要手动做,繁琐易错
  → 需要一个"自动帮你转"的流 → DataStream

普通字节流只能读写字节,写一个 int 要手动拆成 4 个字节、读一个 int 要手动把 4 字节拼回来(繁琐易错)。痛点是「基本类型↔字节的转换要手动做」。所以需要一个「自动帮你转」的流——DataStream。理解「普通字节流只能读写字节、写 int 要手动拆 4 字节读要手动拼、繁琐易错、需要自动转换基本类型和字节的流」,就理解了 DataStream 要解决的问题。

二、DataOutputStream/DataInputStream 的能力

DataStream 提供「基本类型 ↔ 字节」的自动转换:

DataOutputStream(写):
  writeInt(int)       自动把 int 转成 4 字节写出
  writeLong(long)     8 字节
  writeShort(short)   2 字节
  writeDouble(double) 8 字节(IEEE 754)
  writeFloat(float)   4 字节
  writeBoolean(bool)  1 字节
  writeChar(char)     2 字节
  writeByte(int)      1 字节
  writeUTF(String)    字符串(2 字节长度 + UTF-8 内容)

DataInputStream(读):
  readInt()/readLong()/readDouble()/readBoolean()/readUTF()...
  → 对应地把字节读回成基本类型

它们是装饰流(处理流):
  要套在别的流外面
  new DataOutputStream(底层输出流)
  new DataInputStream(底层输入流)
  → 通常套在 Buffered 外面(DataStream 自身不带缓冲)

所以 DataStream = "给字节流加上'读写基本类型'能力的装饰器"

DataStream 提供基本类型↔字节的自动转换:DataOutputStreamwriteInt/writeLong/writeDouble/writeBoolean/writeUTF 等自动把基本类型转字节写出;DataInputStreamreadInt/readLong 等把字节读回基本类型。它们是装饰流(要套在别的流外面,new DataOutputStream(底层流),通常套在 Buffered 外面因为自身不带缓冲)。理解「DataStream 提供基本类型↔字节自动转换(writeInt/readInt/writeUTF 等)、是装饰流套在别的流外面(通常套 Buffered 外)、给字节流加读写基本类型能力」,就理解了它的能力。

三、读写对称:铁律

DataStream 的第一铁律是「读写必须对称」:

关键认知:字节流里的数据没有"类型标记"
  writeInt(42) 写出的就是 4 个字节(00 00 00 2A)
  这 4 个字节本身不知道自己是个 int
  → 读的时候,你必须"知道"这里是个 int,用 readInt() 读 4 字节

所以读写必须严格对称:
  写:writeInt → writeLong → writeUTF
  读:readInt → readLong → readUTF   ← 顺序、类型完全一致

搞错的后果(且通常不报错!):
  写:writeInt(42); writeLong(100);   // 4字节 + 8字节
  读:readLong(); readInt();          // 想先读 8 字节
  → 把 int 的 4 字节 + long 的前 4 字节 当成一个 long 读
  → 读出完全错误的值,且不报错(字节数够,只是拼错了)

所以:
  用 DataStream 定义二进制格式 = 定义"字段的顺序和类型"
  读写代码必须按同样的布局
  → 二进制格式要写清楚"字段布局文档"(第1字段 int、第2字段 long...)

DataStream 的第一铁律是「读写必须对称」——字节流里数据没有类型标记writeInt(42) 写出的就是 4 个字节,字节本身不知道是 int)。所以读的时候必须「知道」这里是 int、用 readInt 读 4 字节。写入和读取的顺序、类型必须完全一致,搞错了会把字节错误拼接、读出错误的值且通常不报错(字节数够只是拼错)。所以用 DataStream 定义二进制格式要写清「字段布局文档」。理解「字节流数据无类型标记、读写必须对称(顺序类型一致)、搞错会拼错且通常不报错、要写清字段布局文档」,就掌握了 DataStream 的核心铁律。

四、字节序:大端

DataStream 的字节序是固定的「大端(Big-Endian)」:

字节序(Endianness):多字节数据在内存/流里的字节排列顺序
  大端(Big-Endian):高位字节在前(低地址)
    int 0x12345678 → 字节序列 12 34 56 78
  小端(Little-Endian):低位字节在前
    int 0x12345678 → 字节序列 78 56 34 12

DataStream 固定用大端:
  writeInt(0x12345678) → 写出 12 34 56 78(高字节在前)
  这也是"网络字节序"(Network Byte Order)—— Java 网络传输的标准

为什么要关心字节序:
  跨语言/跨系统交互时——
  如果对方(如 C 程序在小端机器上)用小端读你写的大端数据
  → 字节顺序反了,读出错误的值
  → 要约定好字节序,或对方按大端读

在纯 Java 环境(都用 DataStream):
  写和读都是大端,一致,没问题

需要小端时:
  DataStream 不支持切换字节序
  → 用 ByteBuffer(可 order(ByteOrder.LITTLE_ENDIAN))自己处理

DataStream 的字节序固定是「大端(Big-Endian,高位字节在前)」——writeInt(0x12345678) 写出 12 34 56 78。这也是「网络字节序」(Java 网络传输标准)。跨语言/跨系统交互时要关心字节序——如果对方(C 程序在小端机器)用小端读你的大端数据,字节顺序反了会读出错误值,要约定好字节序。纯 Java 环境(都用 DataStream 大端)没问题。需要小端时 DataStream 不支持切换,要用 ByteBuffer.order(LITTLE_ENDIAN)。理解「DataStream 字节序固定大端(高位在前,也是网络字节序)、跨语言交互要关心字节序(对方小端读大端会反)、纯 Java 大端一致没问题、需要小端用 ByteBuffer」,就掌握了字节序问题。

五、writeUTF 的特殊之处

writeUTF/readUTF 有个特殊之处,容易踩坑:

writeUTF(String) 的格式:
  2 字节长度(UTF-8 编码后的字节数)+ UTF-8 编码的内容
  → 前面加了长度前缀,读的时候先读长度、再读那么多字节

好处:readUTF 能自动知道字符串多长(靠长度前缀)
  不用像普通字符那样读到某个结束符

坑:
  ① 它是"改进版 UTF-8"(Modified UTF-8):
     和标准 UTF-8 略有不同(如 null 字符、增补字符的编码)
     → 和其他语言/标准 UTF-8 交互时可能不兼容
  ② 长度前缀是 2 字节 → 字符串 UTF-8 后不能超过 65535 字节
     (超了会抛异常)

所以:
  writeUTF/readUTF 适合"Java 内部"读写字符串(配对使用没问题)
  跨语言/大字符串 → 自己用指定编码写字节(如 String.getBytes(UTF_8)
    先写长度再写字节),更可控

writeUTF/readUTF 的格式是「2 字节长度前缀 + UTF-8 内容」——readUTF 靠长度前缀自动知道字符串多长。① 是「改进版 UTF-8」(Modified UTF-8),和标准 UTF-8 略有不同(跨语言交互可能不兼容);② 长度前缀是 2 字节,字符串 UTF-8 后不能超过 65535 字节。所以 writeUTF/readUTF 适合 Java 内部配对使用,跨语言/大字符串要自己用指定编码写字节getBytes(UTF_8) 先写长度再写内容)。理解「writeUTF 格式=2 字节长度前缀+UTF-8 内容(readUTF 靠前缀知长度)、坑是改进版 UTF-8 跨语言可能不兼容+不能超 65535 字节、跨语言用 getBytes 自己处理」,就掌握了 writeUTF 的特殊之处。

六、应用与选择

总结 DataStream 的应用和与其他方式的选择:

DataStream 的应用:
  ① 读写二进制格式文件(自定义的 .dat、.bin)
  ② 简单的自定义二进制协议
  ③ 需要紧凑存储基本类型数据(比文本省空间)

对比其他序列化方式:
  DataStream:手动逐字段读写,紧凑、可控,但要自己管布局
  Java 序列化(ObjectOutputStream):自动序列化整个对象,
    方便但格式冗余、Java 专用、有安全风险
  JSON/文本:可读、跨语言,但占空间、慢
  Protobuf/其他二进制协议:紧凑、跨语言、有 schema,更适合复杂场景

选择:
  简单的、Java 内部的二进制读写 → DataStream
  对象序列化 → 避免 Java 原生序列化,用 JSON/Protobuf
  跨语言二进制协议 → Protobuf 等(DataStream 的字节序/UTF 兼容性是坑)

结论:DataStream 是"手动读写基本类型二进制"的基础工具,
  简单场景够用,复杂/跨语言用专门的序列化框架

DataStream 的应用:读写二进制格式文件、简单自定义二进制协议、紧凑存储基本类型。对比其他序列化:DataStream 手动逐字段读写(紧凑可控但要自己管布局)、Java 序列化(自动但冗余/有安全风险)、JSON(可读跨语言但占空间)、Protobuf(紧凑跨语言有 schema)。选择:简单 Java 内部二进制读写用 DataStream、对象序列化用 JSON/Protobuf 避免 Java 原生序列化、跨语言二进制协议用 Protobuf(DataStream 的字节序/UTF 兼容性是坑)。理解「DataStream 应用:二进制文件/简单协议/紧凑存储;选择:简单 Java 内部用 DataStream、对象序列化用 JSON/Protobuf、跨语言用 Protobuf」,就掌握了 DataStream 的应用和选择。

记忆钩子:「DataInputStream/DataOutputStream=能读写 Java 基本类型的处理流(writeInt/readInt/writeLong/writeDouble/writeBoolean/writeUTF),自动做基本类型↔字节转换,免去手动拆拼字节;是装饰流套在别的流外面(通常套 Buffered 外,自身不带缓冲);★第一铁律读写必须对称(字节流数据无类型标记,写 writeInt 读必须 readInt 按同顺序同类型,搞错拼错且通常不报错);字节序固定大端(高位在前,也是网络字节序),跨语言交互要注意(对方小端读会反),需要小端用 ByteBuffer;writeUTF=2 字节长度前缀+改进版 UTF-8(跨语言可能不兼容、不能超 65535 字节);简单 Java 内部二进制用它、跨语言用 Protobuf」

七、常见误区与追问

  • 误区:DataStream 写出的字节自带类型信息。 不带——writeInt(42) 写出的就是 4 个字节,字节本身不知道是 int;读的时候必须「知道」这里是 int、用 readInt 读 4 字节;所以读写必须严格对称(顺序、类型一致)。
  • 误区:读写顺序/类型搞错了会报错。 通常不报错——如果字节数够,只是把字节错误地拼接成了别的值(比如把 int 的 4 字节和 long 的前 4 字节当一个 long 读),读出完全错误的值却不抛异常;所以要严格保证读写对称、写清字段布局文档。
  • 误区:DataStream 的字节序可以配置。 不能——DataStream 固定用大端(Big-Endian,也是网络字节序);需要小端字节序(如和某些 C 程序交互)时,DataStream 做不到,要用 ByteBuffer 的 order(ByteOrder.LITTLE_ENDIAN) 自己处理。
  • 误区:writeUTF 用的是标准 UTF-8。 是「改进版 UTF-8」(Modified UTF-8)——和标准 UTF-8 在 null 字符、增补字符等的编码上略有不同,跨语言/跨系统交互时可能不兼容;且它前面加 2 字节长度前缀、字符串 UTF-8 后不能超过 65535 字节;跨语言场景建议自己用 getBytes(UTF_8) 处理。
  • 追问:为什么用 DataStream 读写要严格对称? 因为字节流里的数据没有类型标记——writeInt(42) 只是写了 4 个字节,字节不携带「我是个 int」的信息;读的时候 DataStream 靠你调 readInt() 来「解释」这 4 个字节是 int;如果读的顺序或类型和写的不一致,字节会被错误地划分和拼接,读出错误的值且通常不报错。
  • 追问:DataStream 和 Java 对象序列化(ObjectOutputStream)有什么区别? DataStream 手动逐个字段 writeInt/writeUTF 读写基本类型(紧凑、可控,但要自己管字段布局);ObjectOutputStream 自动序列化整个对象图(方便,但格式冗余、Java 专用、有反序列化安全风险);DataStream 适合简单紧凑的二进制读写,对象序列化现在更推荐用 JSON/Protobuf 替代 Java 原生序列化。
  • 追问:需要写小端字节序的二进制数据怎么办? DataStream 只支持大端,不能切换;用 ByteBuffer——buf.order(ByteOrder.LITTLE_ENDIAN) 设成小端,再用 putInt/putLong 等写入,然后把 ByteBuffer 的字节写到流/channel;ByteBuffer 能灵活控制字节序,适合需要小端或跨平台字节序的场景。

八、加强记忆

DataInputStream/DataOutputStream 是「能直接读写 Java 基本类型」的处理流——普通字节流只能读写字节(写 int 要手动拆 4 字节、读要手动拼),DataStream 提供 writeInt/writeLong/writeDouble/writeBoolean/writeUTF 和对应的 readXxx自动做基本类型↔字节的转换。它们是装饰流(套在别的流外面,通常套在 Buffered 外,自身不带缓冲)。第一铁律:读写必须对称——字节流里数据没有类型标记writeInt(42) 写出的就是 4 个字节),读的时候必须按相同的顺序和类型读(writeIntreadInt),搞错会把字节错误拼接、读出错误值且通常不报错(所以要写清字段布局文档)。字节序固定是大端(Big-Endian,高位在前,也是网络字节序)——跨语言/跨系统交互要注意(对方用小端读会反),需要小端要用 ByteBuffer.order(LITTLE_ENDIAN)writeUTF 格式是「2 字节长度前缀 + 改进版 UTF-8」——跨语言可能不兼容、字符串 UTF-8 后不能超 65535 字节,跨语言建议自己用 getBytes(UTF_8)应用:读写二进制文件、简单自定义协议、紧凑存储;简单 Java 内部用 DataStream、跨语言/复杂用 Protobuf。一句话「DataStream 能读写基本类型(writeInt/readInt/writeUTF 自动转字节),是装饰流套 Buffered 外;★读写必须对称(字节无类型标记,搞错拼错且不报错);字节序固定大端(网络字节序),小端用 ByteBuffer;writeUTF 是改进版 UTF-8+长度前缀,跨语言用 Protobuf」。