DataOutputStream.writeUTF() 先写2字节大端序长度前缀,再写modified UTF-8编码内容;该编码将U+0000编码为0xC0 0x80且不支持代理对,故无法正确处理emoji等增补字符。

DataOutputStream.writeUTF() 不是简单地把字符串转成 UTF-8 写进去,它在字节流里加了一个关键结构:**2 字节的长度前缀 + 修改版 UTF-8 编码内容**。这个设计让 readUTF() 能精准定位并还原字符串,但也带来了编码限制和兼容性要求。
长度前缀是 big-endian 的无符号短整型
writeUTF() 先写入 2 个字节,表示后续 UTF 编码字节的**实际长度(不是字符数)**,用大端序存储。比如字符串 "Hi" 编码后占 2 字节,前缀就是 0x00 0x02;"你好" 在 writeUTF 中编码为 6 字节(每个汉字三字节),前缀就是 0x00 0x06。这个值最大为 65535,所以单次 writeUTF 最多写入 65535 字节的编码内容——对应字符数取决于编码长度,中文通常最多约 21845 个。
UTF 编码部分不等于标准 UTF-8
它用的是 Java 自定义的“modified UTF-8”,主要差异有:
- U+0000 到 U+007F:和标准 UTF-8 一致,单字节
- U+0080 到 U+07FF:也和标准 UTF-8 一致,双字节
- U+0800 及以上:仍用三字节,但完全不支持代理对(surrogate pairs)
- U+0000 被编码为 0xC0 0x80(而非 0x00),这是为了区分空字节和字符串结束
这意味着 emoji(如 ?、?)、增补平面汉字(如 ? U+20000)、甚至部分生僻古籍用字,都会被错误截断或变成乱码。
必须配对使用 readUTF() 才能正确读取
因为长度前缀 + 修改版编码是专有格式,不能用通用方式解析:
- 用 FileReader 或 Files.readString(path, UTF_8) 直接读,会把前 2 字节当文本开头,导致乱码或 MalformedInputException
- 用 DataInputStream.readUTF() 才会先读 2 字节得长度,再按 modified UTF-8 规则解码对应字节数
- 即使你只写了一个字符串,也不能跳过前缀直接读内容——长度信息是解码逻辑的一部分
替代方案建议
如果需要完整 Unicode 支持或跨语言/跨平台兼容:
- 写字符串时改用 OutputStreamWriter 配合指定 Charset(如 UTF_8),再套一层 BufferedOutputStream
- 或手动调用 string.getBytes(StandardCharsets.UTF_8) 得到字节数组,用 write(byte[]) 写入,并额外写入 int 类型长度(4 字节)——这样更可控,也兼容标准工具
- writeUTF() 更适合内部二进制协议场景,比如 Java 进程间短字符串通信,且明确控制两端都用 DataInputStream/DataOutputStream











