序列化
约 1009 字大约 3 分钟
布欧-Lewyon
2026-05-16
序列化(Serialization)将 Java 对象转换为字节序列,便于存储或网络传输;反序列化则是逆过程。Java 原生序列化通过 Serializable 接口实现,但现代应用中 JSON 是更主流的方案。
Serializable 接口
Serializable 是一个标记接口(无方法),声明实现该接口的类允许被序列化:
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private long id;
private String name;
private String email;
private transient String password; // 不会被序列化
// getter / setter
}序列化与反序列化:
// 序列化:对象 → 字节
User user = new User(1L, "Alice", "alice@example.com", "secret");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
oos.writeObject(user);
}
// 反序列化:字节 → 对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
User deserialized = (User) ois.readObject();
System.out.println(deserialized.getName()); // Alice
System.out.println(deserialized.getPassword()); // null(transient)
}serialVersionUID
每个可序列化类建议显式声明 serialVersionUID。反序列化时 JVM 会比对 UID,不一致则抛 InvalidClassException:
private static final long serialVersionUID = 1L;- 若不显式声明,JVM 会根据类结构自动生成,但不同编译器的生成策略可能不同。
- 只要类的结构做了不兼容修改(删除字段、改变类型),就应更新 UID。
- 兼容修改(增加字段、增加方法)可保持 UID 不变,新增字段取默认值。
transient 关键字
transient 标记的字段不会被序列化:
private transient String password; // 不序列化
private transient Logger log = ...; // 日志对象无需序列化反序列化后 transient 字段取类型默认值(引用类型为 null,基本类型为 0/false)。
序列化与继承
- 父类实现了
Serializable,子类自动可序列化。 - 父类未实现,但子类实现了:父类字段不会被序列化,反序列化时会调用父类的无参构造器恢复父类状态。如果父类没有无参构造器,抛出
InvalidClassException。
自定义序列化
通过 writeObject / readObject 方法定制序列化行为:
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient String password;
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // 先写默认字段
oos.writeObject(encrypt(password)); // 加密后写入
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // 读默认字段
this.password = decrypt((String) ois.readObject()); // 读取并解密
}
}Java 原生序列化 vs JSON
| 对比维度 | Java 原生序列化 | JSON(Jackson / Gson) |
|---|---|---|
| 可读性 | 二进制,不可读 | 文本,可直接查看 |
| 跨语言 | ❌ 仅 Java | ✅ 几乎所有语言 |
| 版本兼容 | 需管理 serialVersionUID | 忽略未知字段 |
| 性能 | 较慢 | 更快 |
| 安全性 | 反序列化可能执行任意代码(需注意) | 相对安全 |
建议:内部 RPC 或缓存等 Java 间交互可用原生序列化;对外 API、微服务间通信、持久化到数据库等场景优先使用 JSON(Jackson)。
安全注意
反序列化是一个危险操作——字节流中的类会在当前 JVM 中被加载并执行其 readObject 方法。攻击者可构造恶意字节流触发远程代码执行。防范措施:
- 不要反序列化不可信的字节流。
- 使用
ObjectInputFilter(Java 9+)设置白名单/黑名单。 - 考虑用 JSON 替代原生序列化。
小结
- 实现
Serializable接口(标记接口),配合ObjectOutputStream/ObjectInputStream读写。 - 始终显式声明
serialVersionUID,避免编译器自动生成的兼容性隐患。 transient字段跳过序列化,适合密码、连接等敏感或不可序列化的内容。- 原生序列化仅限 Java 内部使用,对外接口优先 JSON。
- 易错:反序列化时
static字段不会被还原(属于类而非对象);单例类实现Serializable后需通过readResolve()方法保证单例约束,否则每次反序列化都会创建新实例。 - 思考任务:定义一个
Book类实现Serializable,包含title、isbn、price字段和一个transient的discount字段。序列化到文件后反序列化,观察discount的值。
上一节:正则表达式
下一节:注解
