Java 序列化与反序列化终极解析
1. 核心概念
(1) 什么是序列化?
定义:将对象转换为字节流的过程(对象 → 字节)
目的:
持久化存储(如保存到文件)
网络传输(如RPC调用)
深拷贝实现
(2) 什么是反序列化?
定义:将字节流还原为对象的过程(字节 → 对象)
关键点:
不会调用构造方法
字段值直接从字节流读取
Q:在java语言中是如何区分class版本的?
A:1.首先通过类的名字,然后在通过序列化版本号进行区分的
注意:在java语言中,不能仅仅通过一个类的名字来进行类的区分,这样太危险了。
2. 核心API
(1) 序列化相关类
类/接口 | 作用 |
---|---|
Serializable |
标记接口(无方法) |
Externalizable |
提供自定义序列化(需实现2个方法) |
ObjectOutputStream |
序列化输出流 |
ObjectInputStream |
反序列化输入流 |
(2) 关键方法
java
// 序列化
objectOutputStream.writeObject(obj);
// 反序列化
Object obj = objectInputStream.readObject();
3. 完整流程解析
(1) 序列化流程
检查对象是否实现
Serializable
递归序列化所有非
transient
字段写入类描述信息(含
serialVersionUID
)写入字段数据
(2) 反序列化流程
读取并验证
serialVersionUID
分配对象内存(不调用构造方法)
递归填充字段值
对于
Externalizable
对象,调用readExternal()
4. 代码示例
(1) 基础序列化
java
// 可序列化类
class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient int age; // 不参与序列化
// 构造方法、getter/setter省略
}
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("person.dat"))) {
oos.writeObject(new Person("Alice", 25));
}
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("person.dat"))) {
Person p = (Person) ois.readObject();
System.out.println(p.getName()); // 输出Alice
System.out.println(p.getAge()); // 输出0(transient字段)
}
(2) 自定义序列化(Externalizable)
java
class Student implements Externalizable {
private String name;
// 必须有无参构造器
public Student() {}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
}
@Override
public void readExternal(ObjectInput in) throws IOException {
this.name = in.readUTF();
}
}
5. 关键机制
(1) serialVersionUID:序列化版本号
作用:版本控制,防止类结构变更导致反序列化失败
生成方式:
显式声明:
private static final long serialVersionUID = 1L;
隐式生成:根据类结构计算hash值
(2) 序列化规则
字段类型 | 是否序列化 | 说明 |
---|---|---|
普通实例字段 | 是 | |
transient 字段 |
否 | 如密码字段 |
static 字段 |
否 | 属于类而非对象 |
未实现Serializable | 抛出异常 | 所有引用字段必须可序列化 |
6. 高级特性
(1) 自定义序列化
java
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 默认序列化
out.writeUTF(encrypt(password)); // 自定义处理
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 默认反序列化
this.password = decrypt(in.readUTF()); // 自定义处理
}
(2) 序列化代理模式
java
private Object writeReplace() {
return new SerializationProxy(this);
}
private static class SerializationProxy implements Serializable {
private final String data;
SerializationProxy(OriginalClass obj) {
this.data = obj.getData();
}
private Object readResolve() {
return new OriginalClass(data);
}
}
7. 常见问题与解决方案
(1) 反序列化漏洞
风险:攻击者可能构造恶意字节流执行任意代码
防护:
使用
ObjectInputFilter
设置白名单
java
ObjectInputFilter filter = info ->
info.serialClass() == Person.class ? Status.ALLOWED : Status.REJECTED;
ois.setObjectInputFilter(filter);
(2) 性能优化
替代方案:
JSON(Jackson/Gson)
二进制协议(Protocol Buffers)
Kryo(高性能Java序列化)
8. 记忆技巧
(1) 核心口诀
"序列化要接口,transient能跳过
static不算数,UID保平安
构造方法不执行,字段直接写内存"
(2) 流程图解
复制
序列化: [对象] → [检查Serializable] → [写入元数据] → [递归写字段] → [字节流] 反序列化: [字节流] → [验证UID] → [分配内存] → [填充字段] → [返回对象]
9. 面试高频问题
Q: 反序列化时如何避免调用构造方法?
A: JVM直接分配内存并从字节流填充数据Q: 为什么
Serializable
是空接口?
A: 标记接口,仅用于类型检查Q: 如何实现深拷贝?
A: 序列化后再反序列化Q:
Externalizable
和Serializable
区别?
A: 前者需实现方法,后者自动序列化
10. 最佳实践
安全性:
敏感字段标记
transient
使用
ObjectInputFilter
版本控制:
始终显式声明
serialVersionUID
性能:
避免序列化大对象
考虑替代方案(如ProtoBuf)
代码健壮性:
实现
readObject()
时保持防御性编程对不可变对象使用序列化代理模式
通过这个完整体系,你已掌握Java序列化的:
✅ 核心机制 ✅ 安全风险 ✅ 性能优化 ✅ 工程实践