你可能遇到过这样的情况:用 DataInputStream 的 readUTF 方法读取字符串时,突然抛出一个 ja va.io.UTFDataFormatException: encoded string too long 的错误。这不是什么奇怪的 bug,而是 readUTF 本身的一道“硬门槛”——它要求字符串的 UTF-8 编码字节长度不能超过 65535(也就是 216−1)。
这个限制的根源在于 readUTF 的工作方式。它先读 2 个字节,这两个字节是一个无符号短整型,用来表示后续 UTF 编码字节的长度。2 个字节能表示的最大值就是 65535,超出这个范围,协议层面就不认了。
这里有个很容易被忽略的点:字符串的字符数和它的 UTF-8 编码字节数,完全是两码事。一个字符串可能只有几万个字符,但里面只要混入中文、emoji 或其他 Unicode 字符,编码后的体积就可能迅速膨胀(一个汉字就要占 3 个字节)。而这个限制跟 JVM 或 JDK 版本无关,是 DataInputStream 规范中雷打不动的强制要求。
如何判断是否真的超限了?
别只盯着 String.length() 看,那个数字是字符数,对判断这个限制没什么帮助。真正要查的是 UTF-8 编码后的字节数:
- 用
string.getBytes(StandardCharsets.UTF_8).length拿到真实的字节长度 - 如果结果大于等于 65536,那在 writeUTF/readUTF 的流程里就一定会失败
- 值得警惕的是:writeUTF 在写入时就会做同样的校验。所以问题往往在写入端就已经埋下了,只是到读取时才暴露出来
替代方案:怎么绕过 readUTF 的限制?
当需要传输长文本时,最直接的办法就是放弃 readUTF,改用更灵活的组合方式。比如:
- 写入端:先写一个 int 类型的长度(
dos.writeInt(len)),再写原始字节(dos.write(bytes)) - 读取端:先读 int 拿到长度,再用
new String(din.readNBytes(len), StandardCharsets.UTF_8)还原字符串 - 或者直接用
BufferedReader/BufferedWriter加行协议,也可以考虑ObjectOutputStream(但要留意序列化开销和兼容性问题)
哪些场景容易踩坑?
有意思的是,这个限制虽然不复杂,但踩坑的人确实不少。以下几个场景尤其容易中招:
- 直接把日志、JSON、XML 这类大文本内容丢进 writeUTF,完全没有预估编码后的体积
- 前后端混用时,Ja va 端用 DataInputStream.readUTF,而另一端(比如 Python 的 socket)手动拼接 UTF-8 字节,却没有做长度截断
- 升级数据格式后,读写逻辑没同步更新。旧版本能存的长字符串,新版本一读就报错
说到底,这个问题不复杂,但确实容易忽略。关键在于区分“字符数”和“UTF-8 字节数”,并且在设计阶段就把传输协议的边界明确下来。
