DataInputStream/DataOutputStream 是干什么的?怎么读写基本类型和处理字节序?
简化版
**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 的方法:
| 写方法 | 读方法 | 类型 | 字节数 |
|---|---|---|---|
| writeInt | readInt | int | 4 |
| writeLong | readLong | long | 8 |
| writeDouble | readDouble | double | 8 |
| writeBoolean | readBoolean | boolean | 1 |
| writeChar | readChar | char | 2 |
| writeUTF | readUTF | 字符串(改进 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 字节;如果顺序或类型搞错了(比如写的时候先writeInt后writeLong,读的时候先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 提供基本类型↔字节的自动转换:DataOutputStream 的 writeInt/writeLong/writeDouble/writeBoolean/writeUTF 等自动把基本类型转字节写出;DataInputStream 的 readInt/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 个字节),读的时候必须按相同的顺序和类型读(writeInt→readInt),搞错会把字节错误拼接、读出错误值且通常不报错(所以要写清字段布局文档)。字节序固定是大端(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」。