Java 序列化:把对象变成字节
更新时间:2026-09-21。序列化关注对象在内存之外的表示,跨系统数据交换更推荐 Protobuf/JSON。本篇讲清原生机制与其风险。
一、什么是序列化
把内存对象转成可存储/传输的字节流叫序列化,反向还原叫反序列化。Java 原生机制要求类实现标记接口 java.io.Serializable(里面没有任何方法,仅起标识作用)。
java
class User implements Serializable {
private static final long serialVersionUID = 1L;
String name;
transient String password; // transient:不参与序列化
}
try (var oos = new ObjectOutputStream(new FileOutputStream("u.bin"))) {
oos.writeObject(new User("tom", "secret"));
}
try (var ois = new ObjectInputStream(new FileInputStream("u.bin"))) {
User u = (User) ois.readObject(); // password 还原后为 null
}二、几条规则
- transient 字段不序列化,反序列化后是默认值(null/0/false),适合缓存、派生字段、敏感信息。
- static 字段不序列化:序列化的是对象实例状态,静态属于类。
- 整个对象图都要可序列化,否则
NotSerializableException;引用里有不能序列化的就标 transient。 - 实现
Externalizable(writeExternal/readExternal)可手动控制,但更繁琐。
三、serialVersionUID:版本指纹
它是类的版本号。反序列化时 JVM 比对字节流与当前类的 UID:
- 不显式声明:编译器根据类结构自动算一个,类稍改(增删字段)UID 就变,反序列化旧数据直接
InvalidClassException。 - 显式固定为 1L:允许类做兼容演进——新增字段反序列化旧数据时给默认值、删除字段被忽略。
正式代码都应显式声明,避免不同环境算出不一致的值。
四、为什么强烈不推荐原生序列化
安全(最致命):readObject 本质是「执行字节流里描述的对象构造」,攻击方构造恶意 gadget 链就能在反序列化时触发任意代码(历史上大量 RCE 漏洞)。绝不要反序列化不可信来源的数据。
其他问题:字节流是 Java 私有格式,不能跨语言;体积大、性能一般、强绑定类结构;JDK 已对部分场景发出警告,长期方向是去序列化。
五、现代替代
| 方案 | 跨语言 | 可读 | 适用 |
|---|---|---|---|
| JSON(Jackson/Gson) | ✅ | 好 | Web API、配置 |
| Protobuf | ✅,二进制紧凑 | 否 | 高性能 RPC |
| MessagePack/Kryo | 部分 | 否 | 内部高性能缓存 |
| JDK Serializable | ❌ | 否 | 仅历史代码、短期本地存储 |
只在「同一套 Java 程序、短期、数据完全可信」时才可能用到原生序列化。
六、常见坑
- 漏写 serialVersionUID:类一改,旧数据全部反序列化失败。
- 反序列化外部数据:RCE 风险,杜绝。
- 以为 transient/static 会被保存:都不会。
- DTO 里混入不可序列化字段:writeObject 时抛 NotSerializableException。
- 把序列化当长期存储格式:类演进后极难兼容,应选 JSON/Protobuf。
总结
Java 原生序列化靠实现空标记接口 Serializable 配合 ObjectOutputStream/ObjectInputStream 完成,transient 与 static 字段不参与,serialVersionUID 是版本指纹、不显式固定则类一改旧数据就反序列化失败;致命问题是 readObject 反序列化不可信字节可触发任意代码(反序列化 RCE),且格式私有不可跨语言、体积大,因此除可信环境历史代码外都应改用 JSON 或 Protobuf,并坚持显式声明 serialVersionUID。