序列化和反序列化
java 序列化和反序列化是将对象在 “内存状态” 与 “字节流” 之间转换的过程,用于对象的持久化存储(如写入文件)或网络传输。
一、序列化过程(对象 → 字节流)
序列化是将对象的状态转换为字节序列的过程,核心步骤如下:
| 步骤 | 具体操作 | 细节说明 |
|---|---|---|
| 1. 可序列化检查 | 验证对象所属类是实现 java.io.Serializable 接口 | 若未实现,直接抛出 NotSerializableException;Externalizable 是 Serializable 的子接口,需额外处理。 |
2. 生成 / 获取 serialVersionUID | 若类显式声明 serialVersionUID,则使用该值;否则由编译器根据类结构(字段、方法、继承关系等)生成哈希值作为 serialVersionUID | 该值用于版本兼容性校验,是序列化的 “版本标识”。 |
| 3. 遍历对象图 | 从根对象开始,递归遍历所有引用的对象,形成 “对象图” | 同时维护对象引用表,记录已序列化的对象,避免循环引用导致的无限序列化(如对象 A 引用对象 B,对象 B 又引用对象 A)。 |
| 4. 写入类元数据 | 将类的全限定名、serialVersionUID、字段信息(类型、名称)、方法签名等写入字节流 | 元数据用于反序列化时确认类的结构。 |
| 5. 写入字段值 | 按顺序写入对象的非transient、非static字段值 | - 基本类型(int、boolean等)直接写入字节流;- 对象类型字段则递归执行序列化过程(重复步骤 1-5);- transient和static字段会被忽略,不写入字节流。 |
二、反序列化过程(字节流 → 对象)
反序列化是将字节序列恢复为对象的过程,核心步骤如下:
| 步骤 | 具体操作 | 细节说明 |
|---|---|---|
| 1. 读取类元数据 | 从字节流中读取类的全限定名、serialVersionUID、字段信息等 | 确认要反序列化的类结构。 |
2. serialVersionUID 校验 | 对比字节流中的 serialVersionUID 与当前类的 serialVersionUID | 若不一致,抛出 InvalidClassException,反序列化失败。 |
| 3. 创建对象实例 | - 若类实现 Serializable:通过 Java 内部机制创建对象(不调用构造方法);- 若类实现 Externalizable:必须调用无参构造方法(否则抛出异常) | Externalizable 要求类必须有无参构造,因为它需要手动控制序列化逻辑。 |
| 4. 恢复字段值 | 根据类元数据,从字节流中读取字段值并赋值给对象 | - 对象类型字段递归执行反序列化(重复步骤 1-4);- transient 字段赋予类型默认值(如 int 为 0,对象为 null);- static 字段不会从字节流恢复,保持当前类的 static 字段值。 |
| 5. 处理对象引用 | 通过 “对象引用表” 还原对象间的引用关系 | 确保循环引用或重复引用的对象在反序列化后,引用关系与原对象图一致。 |
三、特殊机制与注意事项
-
Externalizable接口:是Serializable的子接口,要求类必须实现writeExternal和readExternal方法,手动控制序列化 / 反序列化逻辑(更灵活但需自己处理所有字段)。例如:class User implements Externalizable { private String name; private transient int age; // 必须有无参构造 public User() {} @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(name); out.writeInt(age); // 即使 age 是 transient,也可手动写入 } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { name = (String) in.readObject(); age = in.readInt(); } } -
继承关系的序列化:若父类实现
Serializable,子类会自动继承序列化能力;若父类未实现,子类必须手动处理父类字段的序列化(否则父类字段会丢失)。 -
对象引用与循环引用:序列化时会记录对象的引用关系,反序列化时通过 “引用表” 确保同一对象不会被多次创建,循环引用的对象能正确还原引用关系。
总得来说,序列化是 “记录对象结构 + 字段值 + 引用关系” 的过程,反序列化是 “校验版本 + 还原结构 + 赋值字段 + 恢复引用” 的过程,二者通过 serialVersionUID 和对象引用表保障版本兼容与引用正确性。
transient
在 Java 中,transient 是一个关键字,用于控制对象序列化时的成员变量持久化行为,核心作用是标记变量不需要被序列化。
1. 序列化与 transient 的关系
序列化是将对象状态转换为字节流(用于网络传输、磁盘存储)的过程;反序列化则是字节流恢复为对象的过程。而被 transient 修饰的变量,在序列化时会被忽略(即不会将其值写入字节流);反序列化后,该变量会被赋予类型默认值(如 int 为 0,对象为 null)。
2. 典型使用场景
- 敏感数据保护:如用户密码、密钥等信息,不希望被序列化后持久化(防止泄露)。
- 临时 / 缓存数据:如对象的临时计算结果、缓存集合等,无需持久化,序列化时可忽略。
示例代码:
class User implements Serializable {
private String username;
private transient String password; // 密码不参与序列化
public User(String username, String password) {
this.username = username;
this.password = password;
}
}
// 序列化时,password的值不会被保存;反序列化后,password为null
3. 注意事项
transient仅修饰成员变量,不能修饰方法、类或静态变量(static变量本身不参与对象序列化)。- 反序列化后,
transient变量需手动赋值(或通过类的初始化逻辑处理),否则为默认值。
简言之,transient 是 Java 用于 “选择性忽略对象成员变量序列化” 的工具,适用于敏感信息或临时数据的场景。
serialVersionUID
在我们看源码的过程当中,常常能看到这个**serialVersionUID** 东西,这里可以仅作了解。
@java.io.Serial
private static final long serialVersionUID = -7720805057305804111L;
作用
在 Java 中,serialVersionUID 是用于控制对象序列化与反序列化版本兼容性的标识符,核心作用是验证序列化类和反序列化类的版本是否一致,避免因类结构变化导致反序列化失败。
1. 序列化与版本兼容的背景
Java 序列化是将对象转换为字节流(用于存储或网络传输)的过程,反序列化则是将字节流还原为对象。若类的结构(如字段、方法、继承关系)发生变化,默认情况下 Java 会自动生成新的serialVersionUID,此时新旧版本类会因版本不兼容而抛出 InvalidClassException。
2. serialVersionUID 的核心作用
- 显式版本控制:手动指定
serialVersionUID后,即使类结构发生兼容变化(如新增非必填字段、修改方法逻辑),只要serialVersionUID不变,反序列化仍可成功(新字段取默认值)。 - 避免隐藏错误:若类结构发生不兼容变化(如删除字段、修改字段类型),需手动修改
serialVersionUID,明确告知系统 “版本不兼容”,防止因自动生成的版本号变化导致的隐性反序列化失败。
3. 手动指定 vs 自动生成
| 场景 | 手动指定 serialVersionUID | 不指定(Java 自动生成) |
|---|---|---|
| 版本兼容性控制 | 开发者自主控制,兼容变化时保持版本号不变 | 类结构变化时版本号自动变化,极易导致不兼容 |
| 反序列化健壮性 | 明确可控,减少因版本问题导致的 InvalidClassException | 隐藏风险,类结构微小变化都可能触发反序列化失败 |
4. 示例说明
// 版本1:手动指定 serialVersionUID
class User implements Serializable {
private static final long serialVersionUID = 1L; // 显式版本号
private String username;
private int age;
}
// 版本2:新增非必填字段(兼容变化),保持 serialVersionUID 不变
class User implements Serializable {
private static final long serialVersionUID = 1L; // 版本号不变
private String username;
private int age;
private String email; // 新增字段
}
// 反序列化时,版本1的对象可正常恢复,email 为 null(兼容)
若版本 2 未手动指定 serialVersionUID,Java 会自动生成新的版本号,此时版本 1 的序列化对象反序列化时会因版本不兼容抛出异常。
总结
serialVersionUID 是 Java 序列化的 “版本契约”,建议手动指定以明确控制版本兼容性,避免因类结构变化导致的反序列化失败,提升系统的健壮性和可维护性。
java如何使用的(具体流程)
在 Java 反序列化过程中,serialVersionUID 的核心作用是验证 “序列化时的类版本” 与 “当前类版本” 是否兼容,具体流程如下:
1. 反序列化时的版本校验机制
当字节流(序列化后的对象数据)被反序列化时,Java 会执行以下步骤:
- 步骤 1:从字节流中读取序列化时记录的
serialVersionUID(即 “旧版本类” 的版本号)。 - 步骤 2:获取当前类的
serialVersionUID(即 “新版本类” 的版本号)。 - 步骤 3:对比两个版本号:
- 若一致:继续反序列化,将字节流数据映射到当前类的对象中(兼容变化的字段取默认值,如新增字段为
null或0)。 - 若不一致:直接抛出
InvalidClassException,反序列化失败。
- 若一致:继续反序列化,将字节流数据映射到当前类的对象中(兼容变化的字段取默认值,如新增字段为
2. 不同场景下的行为差异
| 场景分类 | serialVersionUID 处理逻辑 | 反序列化结果 |
|---|---|---|
| 手动指定版本号,类结构兼容变化(如新增非必填字段) | 保持 serialVersionUID 不变,反序列化时忽略新增字段(或赋默认值)。 | 成功,旧数据正常恢复,新字段为默认值。 |
| 手动指定版本号,类结构不兼容变化(如删除字段、修改字段类型) | 需手动修改 serialVersionUID,此时版本号不一致,反序列化直接失败。 | 抛出 InvalidClassException。 |
| 未手动指定版本号,类结构任意变化 | Java 自动生成新的 serialVersionUID(基于类结构的哈希值),与旧版本号必然不一致。 | 抛出 InvalidClassException。 |
3. 示例说明
// 版本1:手动指定 serialVersionUID = 1L
class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
}
// 序列化 User 对象:user = new User("Alice", 25);
// 字节流中记录 serialVersionUID = 1L
// 版本2:新增字段(兼容变化),保持 serialVersionUID = 1L
class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private String email; // 新增字段
}
// 反序列化时:
// - name、age 从字节流中恢复为 "Alice"、25;
// - email 为默认值 null(因为序列化时无该字段)。
若版本 2 未手动指定 serialVersionUID,Java 会自动生成新的版本号(如 2L),反序列化时因版本不兼容直接抛出 InvalidClassException。
总结
serialVersionUID 在反序列化中是 “版本契约” 的核心:一致则兼容恢复,不一致则直接失败。手动指定版本号可明确控制类的版本兼容性,避免因类结构微小变化导致的隐性反序列化故障,是保障分布式系统、持久化对象版本兼容的关键手段。
对象引用表
在 Java 序列化过程中,对象引用表(Object Reference Table) 是一个临时维护的内存数据结构,用于跟踪已序列化的对象,解决重复引用和循环引用问题(避免同一对象被多次序列化,或循环引用导致的无限递归)。它的核心作用是:为每个首次序列化的对象分配唯一标识,后续遇到相同对象时仅写入标识而非重复序列化对象内容。
对象引用表的结构
对象引用表本质是一个 “有序列表(或数组)”,结构非常简单:
- 存储内容:按对象首次被序列化的顺序,依次存储对象的引用(内存地址的逻辑映射)。
- 标识规则:每个对象对应一个唯一的整数标识(ID),从
0或1开始递增(不同 Java 版本可能从 0 或 1 起始,核心是递增唯一)。 - 查询方式:通过对象引用快速查找其在表中的标识(类似哈希表的键值映射,键为对象引用,值为标识)。
一、序列化过程中对象引用表的工作流程
假设我们有两个对象 A 和 B,其中 A 引用 B,B 又引用 A(循环引用),序列化过程中引用表的变化如下:
步骤 1:初始化引用表
序列化开始时,引用表为空,标识计数器从 0 开始。
步骤 2:序列化根对象 A
- 检查
A是否在引用表中:不在。 - 将
A加入引用表,分配标识0(此时表中:0 → A)。 - 序列化
A的类元数据(类名、serialVersionUID等)和非transient字段。
步骤 3:序列化 A 的字段(包含对 B 的引用)
A的字段中包含B的引用,检查B是否在引用表中:不在。- 将
B加入引用表,分配标识1(此时表中:0 → A,1 → B)。 - 序列化
B的类元数据和非transient字段。
步骤 4:序列化 B 的字段(包含对 A 的引用)
B的字段中包含A的引用,检查A是否在引用表中:在(标识为0)。- 不再重新序列化
A,而是向字节流中写入一个 “引用标记”(如TC_REFERENCE)和A的标识0。
最终引用表状态
| 标识(ID) | 对应对象 |
|---|---|
| 0 | A |
| 1 | B |
字节流中引用的表示方式
序列化后的字节流中,对象引用不会存储完整的对象数据,而是通过特殊标记 + 标识 ID表示。Java 序列化协议定义了多种标记(通过ObjectStreamConstants实现),其中与引用相关的核心标记包括:
TC_REFERENCE(值为0x71):表示这是一个对象引用,后续会跟随该对象在引用表中的标识 ID。TC_NULL(值为0x70):表示空引用。
例如,上述循环引用场景中,字节流中 B 对 A 的引用会被记录为:TC_REFERENCE + 0(表示 “引用标识为 0 的对象”)。
反序列化时引用表的作用
反序列化过程中,会重建一个反序列化引用表,流程与序列化对称:
- 读取字节流中 “新对象” 时(非引用标记),创建对象实例,加入反序列化引用表并分配标识。
- 读取到
TC_REFERENCE + ID时,直接从反序列化引用表中获取标识为ID的对象,而非重新创建,从而还原原对象间的引用关系。
总的来说:对象引用表是序列化过程中解决 “重复 / 循环引用” 的核心机制,结构上是 “有序列表 + 哈希映射”(列表存对象顺序,哈希映射用于快速查询对象标识),通过 “首次序列化存对象 + 后续引用存标识” 的方式,确保字节流体积最小化,同时正确还原对象间的引用关系。
简单说,它就像一个 “对象通讯录”:每个对象第一次出现时登记一个唯一编号,后续提到这个对象时,只需要说 “编号 X 的对象” 即可,无需重复描述对象详情。
二、序列化过程中对象引用表的工作流程
在反序列化过程中,对象引用表(Deserialization Reference Table)的核心作用是还原序列化前的对象引用关系,确保重复引用的对象只被创建一次,循环引用的对象能正确关联。其工作机制与序列化时的引用表对称,但流程是 “从字节流重建对象→记录引用→还原关联”。
反序列化引用表的结构
反序列化时的引用表与序列化时结构一致,是一个 *“有序列表 + 哈希映射”:
- 有序列表:按对象被反序列化的顺序存储对象实例,每个对象对应一个唯一的整数标识(ID)(与序列化时的 ID 一一对应,从 0 或 1 开始递增)。
- 哈希映射:快速通过 “序列化时的 ID” 查找对应的对象实例(因为字节流中记录的是序列化时的 ID,反序列化时需通过该 ID 定位对象)。
反序列化过程中引用表的工作流程:
以 “序列化时的 A 和 B 循环引用” 为例(A 引用 B,B 引用 A,序列化时 A 的 ID 为 0,B 的 ID 为 1),反序列化时引用表的工作步骤如下:
步骤 1:初始化反序列化引用表
反序列化开始时,引用表为空,ID 计数器从 0 开始(与序列化时的起始 ID 一致)。
步骤 2:读取 “新对象 A” 并加入引用表
- 字节流中首先读取到 “新对象 A” 的标记(非引用标记,如
TC_OBJECT),表示这是一个未被反序列化过的对象。 - 反序列化 A:创建 A 的实例(不调用构造方法),恢复 A 的类元数据和非
transient字段(此时 A 中引用 B 的字段暂时为null,因为 B 还未被反序列化)。 - 将 A 加入引用表,分配 ID=0(与序列化时 A 的 ID 一致),此时表中:
0 → A。
步骤 3:读取 “新对象 B” 并加入引用表
- 继续读取字节流,遇到 “新对象 B” 的标记(
TC_OBJECT)。 - 反序列化 B:创建 B 的实例,恢复 B 的类元数据和非
transient字段(此时 B 中引用 A 的字段暂时为null)。 - 将 B 加入引用表,分配 ID=1(与序列化时 B 的 ID 一致),此时表中:
0 → A,1 → B。
步骤 4:处理 B 中对 A 的引用(引用标记)
- 继续读取 B 的字段时,遇到字节流中的 “引用标记”(
TC_REFERENCE)和 ID=0(表示 “引用序列化时 ID 为 0 的对象”)。 - 反序列化逻辑查询引用表,发现 ID=0 对应的对象是 A,于是将 B 中引用 A 的字段赋值为 A 的实例。
步骤 5:处理 A 中对 B 的引用(引用标记)
- 回到 A 的字段处理(若 A 的字段在 B 之后读取,取决于序列化时的字段顺序),遇到字节流中的 “引用标记” 和 ID=1。
- 查询引用表,ID=1 对应 B,于是将 A 中引用 B 的字段赋值为 B 的实例。
最终结果
A 和 B 的引用关系完全还原:A 引用 B,B 引用 A,与序列化前的对象图一致,且 A 和 B 都只被创建了一次。
特殊场景的处理
- 重复引用同一对象:若多个对象引用同一个 C(如 A 引用 C,B 也引用 C),序列化时 C 的 ID 为 2。反序列化时,C 首次被读取后加入表(ID=2),后续 A 和 B 中对 C 的引用会被解析为 “引用 ID=2 的对象”,最终 A 和 B 的 C 字段指向同一个实例。
- 空引用(TC_NULL):字节流中若遇到
TC_NULL标记(表示空引用),反序列化时直接将对应字段赋值为null,不涉及引用表。 - 父类对象的引用:若子类对象引用父类对象(未实现
Serializable),反序列化时父类对象会被单独创建并加入引用表,确保子类字段对父类的引用正确关联。
核心价值
反序列化引用表通过 “ID 映射 + 顺序记录” 的方式,解决了两个关键问题:
- 避免重复创建:同一对象在字节流中无论被引用多少次,反序列化时只创建一次,节省内存。
- 还原引用关系:无论是单向引用、重复引用还是循环引用,都能完全还原序列化前的对象图结构,保证对象状态的一致性。
简单说,反序列化引用表就像 “拼图指南”:序列化时记录了每个对象的 “编号” 和 “位置”,反序列化时按编号找到对应的对象,再按指南拼出完整的对象关系图。
源码位置
Java 序列化和反序列化的核心逻辑主要在 java.io.ObjectOutputStream(序列化)和 java.io.ObjectInputStream(反序列化)两个类中实现。
- 序列化核心类:
java/io/ObjectOutputStream.java - 反序列化核心类:
java/io/ObjectInputStream.java
核心源码逻辑定位(关键方法)
1. 序列化核心逻辑(ObjectOutputStream)
序列化的入口是 writeObject(Object obj) 方法,其核心逻辑在内部方法 writeObject0(Object obj, boolean unshared) 中实现,主要流程包括:
- 检查对象是否可序列化(是否实现
Serializable); - 处理特殊类型(字符串、数组、枚举等);
- 处理对象引用(通过引用表记录,避免重复序列化);
- 写入类元数据(
classDesc)和字段值。
关键代码片段(简化):
private void writeObject0(Object obj, boolean unshared) throws IOException {
// ... 省略前置检查
if (obj instanceof String) {
writeString((String) obj, unshared); // 处理字符串
} else if (cl.isArray()) {
writeArray(obj, desc, unshared); // 处理数组
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared); // 处理普通可序列化对象
} else {
throw new NotSerializableException(cl.getName()); // 不可序列化则抛异常
}
// ...
}
// 处理普通对象的核心方法
private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared) throws IOException {
// 写入对象标记(TC_OBJECT)
bout.writeByte(TC_OBJECT);
// 写入类元数据(类描述符)
writeClassDesc(desc, false);
// 处理对象引用表:记录当前对象,分配ID
handles.assign(unshared ? null : obj);
// 写入对象字段值
if (desc.isExternalizable() && !desc.isProxy()) {
((Externalizable) obj).writeExternal(this); // 若实现Externalizable,调用其方法
} else {
writeSerialData(obj, desc); // 否则默认序列化字段
}
}
2. 反序列化核心逻辑(ObjectInputStream)
反序列化的入口是 readObject() 方法,核心逻辑在 readObject0(boolean unshared) 中实现,主要流程包括:
- 读取字节流中的标记(如
TC_OBJECT表示新对象,TC_REFERENCE表示引用); - 若为新对象:读取类元数据,创建实例,恢复字段值,加入引用表;
- 若为引用:从引用表中查找对应对象,还原引用关系。
关键代码片段(简化):
private Object readObject0(boolean unshared) throws IOException {
// 读取字节流中的标记(如TC_OBJECT、TC_REFERENCE等)
byte tc = bin.readByte();
switch (tc) {
case TC_OBJECT:
return readOrdinaryObject(unshared); // 处理新对象
case TC_REFERENCE:
return readReference(unshared); // 处理引用
// ... 其他类型(字符串、数组等)的处理
default:
throw new IOException("Invalid type code: " + tc);
}
}
// 处理新对象的核心方法
private Object readOrdinaryObject(boolean unshared) throws IOException {
// 读取类元数据(类描述符)
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
// 创建对象实例(不调用构造方法)
Object obj = desc.isInstantiable() ? desc.newInstance() : null;
// 将对象加入引用表,分配ID
handles.assign(unshared ? null : obj);
// 恢复字段值
if (desc.isExternalizable()) {
((Externalizable) obj).readExternal(this); // 若实现Externalizable,调用其方法
} else {
readSerialData(obj, desc); // 否则默认恢复字段
}
return obj;
}
// 处理引用的方法
private Object readReference(boolean unshared) throws IOException {
int handle = bin.readInt(); // 读取引用ID
Object obj = handles.getObject(handle); // 从引用表中获取对象
// ...
return obj;
}
其他关键类
ObjectStreamClass:存储类的元数据(serialVersionUID、字段信息等),源码位于java/io/ObjectStreamClass.java。HandleTable:对象引用表的具体实现(ObjectOutputStream和ObjectInputStream的内部类),负责管理对象 ID 与实例的映射。
查看建议
- 从
ObjectOutputStream.writeObject()和ObjectInputStream.readObject()作为入口,逐步跟踪到writeObject0和readObject0; - 关注
handle相关逻辑(如handles.assign()、handles.getObject()),这是对象引用表的核心操作; - 注意区分
Serializable和Externalizable的不同处理逻辑(前者自动序列化字段,后者手动控制)。
通过阅读这些源码,可以清晰看到之前讨论的 “对象引用表”“版本校验”“字段序列化” 等机制的具体实现。
4083

被折叠的 条评论
为什么被折叠?



