对象流与序列化
对象流用于把 Java 对象写入字节流,或者从字节流中恢复 Java 对象。这个过程称为序列化和反序列化。
# 1. 什么是序列化
序列化是把对象转换为字节序列的过程。
反序列化是把字节序列恢复为对象的过程。
Java对象 -> ObjectOutputStream -> 字节
字节 -> ObjectInputStream -> Java对象
对象要支持 Java 原生序列化,类需要实现 Serializable 接口。
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
}
Serializable 是标记接口,没有方法。
# 2. 写入对象
try (ObjectOutputStream output = new ObjectOutputStream(
new FileOutputStream("user.obj"))) {
User user = new User(1L, "Alice");
output.writeObject(user);
}
# 3. 读取对象
try (ObjectInputStream input = new ObjectInputStream(
new FileInputStream("user.obj"))) {
User user = (User) input.readObject();
}
readObject() 返回 Object,需要强制类型转换,并可能抛出 ClassNotFoundException。
# 4. serialVersionUID
serialVersionUID 用于标识类的序列化版本。反序列化时,JVM 会检查字节流中的版本号和当前类是否一致。
建议显式声明:
private static final long serialVersionUID = 1L;
如果不声明,JVM 会根据类结构自动计算。一旦字段、方法、访问修饰符发生变化,自动计算结果可能改变,导致旧数据无法反序列化。
# 5. transient
transient 修饰的字段不会被序列化。
private transient String password;
适合不希望持久化的敏感信息、缓存字段、临时计算结果等。
# 6. 序列化对象图
对象流会序列化整个对象图。如果 User 中引用了 Address,那么 Address 也需要可序列化。
public class User implements Serializable {
private Address address;
}
public class Address implements Serializable {
private String city;
}
如果某个非 transient 字段不可序列化,会抛出 NotSerializableException。
# 7. Java 原生序列化的问题
Java 原生序列化在生产环境中要谨慎使用,原因包括:
- 格式只适合 Java,跨语言能力很差。
- 字节内容可读性差,不利于排查问题。
- 类结构变化后兼容性复杂。
- 反序列化存在安全风险。
- 性能和体积通常不如现代序列化框架。
很多安全漏洞都和不可信数据的反序列化有关。
# 8. 替代方案
生产系统中常见替代方案:
- JSON:可读性好,适合接口和配置。
- Protocol Buffers:性能好,跨语言,适合 RPC。
- Avro:适合大数据生态和 Schema 演进。
- Kryo:性能较好,但跨语言能力有限。
- Hessian:曾常用于 Java RPC 场景。
选型时要考虑跨语言、性能、体积、可读性、Schema 演进和安全性。
# 专家实践清单
| 检查项 | 建议 | 风险 |
|---|---|---|
| 数据来源 | 只反序列化可信数据 | 不可信输入可能触发漏洞 |
| 类型范围 | 配置反序列化过滤器 | 任意类型实例化风险高 |
| 版本兼容 | 显式声明 serialVersionUID | 类结构变化导致读取失败 |
| 对象图大小 | 限制深度和数量 | 内存放大或 OOM |
| 替代方案 | 优先 JSON/Protobuf 等 | 原生序列化生态弱 |
对象流的隐含成本:
业务对象
│
├─ 类名、字段描述
├─ 对象引用关系
├─ 字段值
└─ 序列化协议元数据
▼
字节流
# Tips 快问快答
Q:对象流解决什么问题? A:把 Java 对象图序列化为字节,并从字节恢复对象。
Q:为什么生产环境谨慎使用原生序列化? A:安全风险、兼容性、性能、跨语言和可观测性都比较差。
Q:serialVersionUID 有什么作用?
A:标识序列化版本,帮助判断类结构是否兼容。
Q:不声明 serialVersionUID 会怎样?
A:JVM 会自动计算,类结构变化后可能导致旧数据无法反序列化。
Q:transient 的作用是什么?
A:标记字段不参与默认序列化,常用于敏感信息或可重新计算字段。
Q:反序列化最大安全风险是什么? A:攻击者构造恶意对象链,在反序列化过程中触发危险代码路径。
Q:如何降低原生反序列化风险? A:限制数据来源、配置对象输入过滤器、白名单类型、限制深度和大小。
Q:常见替代方案有哪些? A:JSON、Protocol Buffers、Avro、Thrift、Kryo 等。
# 9. 总结
对象流可以方便地保存和恢复 Java 对象,但它不是现代分布式系统中的首选序列化方案。学习它有助于理解对象如何变成字节,但生产环境要特别关注兼容性和反序列化安全。