Java 序列化与反序列化终极解析
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()); // 输出AliceSystem.out.println(p.getAge()); // 输出0(transient字段)
}
(2) 自定义序列化(Externalizable)
java
class Student implements Externalizable {private String name;// 必须有无参构造器public Student() {}@Overridepublic void writeExternal(ObjectOutput out) throws IOException {out.writeUTF(name);}@Overridepublic 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序列化的:
✅ 核心机制 ✅ 安全风险 ✅ 性能优化 ✅ 工程实践