Java 序列化和反序列化过程

序列化和反序列化

java 序列化和反序列化是将对象在 “内存状态” 与 “字节流” 之间转换的过程,用于对象的持久化存储(如写入文件)或网络传输。

一、序列化过程(对象 → 字节流)

序列化是将对象的状态转换为字节序列的过程,核心步骤如下:

步骤具体操作细节说明
1. 可序列化检查验证对象所属类是实现 java.io.Serializable 接口若未实现,直接抛出 NotSerializableExceptionExternalizable 是 Serializable 的子接口,需额外处理。
2. 生成 / 获取 serialVersionUID若类显式声明 serialVersionUID,则使用该值;否则由编译器根据类结构(字段、方法、继承关系等)生成哈希值作为 serialVersionUID该值用于版本兼容性校验,是序列化的 “版本标识”。
3. 遍历对象图从根对象开始,递归遍历所有引用的对象,形成 “对象图”同时维护对象引用表,记录已序列化的对象,避免循环引用导致的无限序列化(如对象 A 引用对象 B,对象 B 又引用对象 A)。
4. 写入类元数据将类的全限定名、serialVersionUID、字段信息(类型、名称)、方法签名等写入字节流元数据用于反序列化时确认类的结构。
5. 写入字段值按顺序写入对象的非transient、非static字段值- 基本类型(intboolean等)直接写入字节流;- 对象类型字段则递归执行序列化过程(重复步骤 1-5);- transientstatic字段会被忽略,不写入字节流。

二、反序列化过程(字节流 → 对象)

反序列化是将字节序列恢复为对象的过程,核心步骤如下:

步骤具体操作细节说明
1. 读取类元数据从字节流中读取类的全限定名、serialVersionUID、字段信息等确认要反序列化的类结构。
2. serialVersionUID 校验对比字节流中的 serialVersionUID 与当前类的 serialVersionUID若不一致,抛出 InvalidClassException,反序列化失败。
3. 创建对象实例- 若类实现 Serializable:通过 Java 内部机制创建对象(不调用构造方法);- 若类实现 Externalizable:必须调用无参构造方法(否则抛出异常)Externalizable 要求类必须有无参构造,因为它需要手动控制序列化逻辑。
4. 恢复字段值根据类元数据,从字节流中读取字段值并赋值给对象- 对象类型字段递归执行反序列化(重复步骤 1-4);- transient 字段赋予类型默认值(如 int 为 0,对象为 null);- static 字段不会从字节流恢复,保持当前类的 static 字段值。
5. 处理对象引用通过 “对象引用表” 还原对象间的引用关系确保循环引用或重复引用的对象在反序列化后,引用关系与原对象图一致。

三、特殊机制与注意事项

  1. 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();
        }
    }
    
  2. 继承关系的序列化:若父类实现 Serializable,子类会自动继承序列化能力;若父类未实现,子类必须手动处理父类字段的序列化(否则父类字段会丢失)。

  3. 对象引用与循环引用:序列化时会记录对象的引用关系,反序列化时通过 “引用表” 确保同一对象不会被多次创建,循环引用的对象能正确还原引用关系。

总得来说,序列化是 “记录对象结构 + 字段值 + 引用关系” 的过程,反序列化是 “校验版本 + 还原结构 + 赋值字段 + 恢复引用” 的过程,二者通过 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 引用 BB 又引用 A(循环引用),序列化过程中引用表的变化如下:

步骤 1:初始化引用表

序列化开始时,引用表为空,标识计数器从 0 开始。

步骤 2:序列化根对象 A

  • 检查 A 是否在引用表中:不在
  • 将 A 加入引用表,分配标识 0(此时表中:0 → A)。
  • 序列化 A 的类元数据(类名、serialVersionUID 等)和非 transient 字段。

步骤 3:序列化 A 的字段(包含对 B 的引用)

  • A 的字段中包含 B 的引用,检查 B 是否在引用表中:不在
  • 将 B 加入引用表,分配标识 1(此时表中:0 → A1 → B)。
  • 序列化 B 的类元数据和非 transient 字段。

步骤 4:序列化 B 的字段(包含对 A 的引用)

  • B 的字段中包含 A 的引用,检查 A 是否在引用表中:(标识为 0)。
  • 不再重新序列化 A,而是向字节流中写入一个 “引用标记”(如 TC_REFERENCE)和 A 的标识 0

最终引用表状态

标识(ID)对应对象
0A
1B

字节流中引用的表示方式

序列化后的字节流中,对象引用不会存储完整的对象数据,而是通过特殊标记 + 标识 ID表示。Java 序列化协议定义了多种标记(通过ObjectStreamConstants实现),其中与引用相关的核心标记包括:

  • TC_REFERENCE(值为 0x71):表示这是一个对象引用,后续会跟随该对象在引用表中的标识 ID。
  • TC_NULL(值为 0x70):表示空引用。

例如,上述循环引用场景中,字节流中 B 对 A 的引用会被记录为:TC_REFERENCE + 0(表示 “引用标识为 0 的对象”)。

反序列化时引用表的作用

反序列化过程中,会重建一个反序列化引用表,流程与序列化对称:

  1. 读取字节流中 “新对象” 时(非引用标记),创建对象实例,加入反序列化引用表并分配标识。
  2. 读取到 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 → A1 → 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 都只被创建了一次。

特殊场景的处理

  1. 重复引用同一对象:若多个对象引用同一个 C(如 A 引用 C,B 也引用 C),序列化时 C 的 ID 为 2。反序列化时,C 首次被读取后加入表(ID=2),后续 A 和 B 中对 C 的引用会被解析为 “引用 ID=2 的对象”,最终 A 和 B 的 C 字段指向同一个实例。
  2. 空引用(TC_NULL):字节流中若遇到TC_NULL标记(表示空引用),反序列化时直接将对应字段赋值为null,不涉及引用表。
  3. 父类对象的引用:若子类对象引用父类对象(未实现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:对象引用表的具体实现(ObjectOutputStreamObjectInputStream的内部类),负责管理对象 ID 与实例的映射。

查看建议

  1. 从 ObjectOutputStream.writeObject() 和 ObjectInputStream.readObject() 作为入口,逐步跟踪到 writeObject0 和 readObject0
  2. 关注 handle 相关逻辑(如 handles.assign()handles.getObject()),这是对象引用表的核心操作;
  3. 注意区分 Serializable 和 Externalizable 的不同处理逻辑(前者自动序列化字段,后者手动控制)。

通过阅读这些源码,可以清晰看到之前讨论的 “对象引用表”“版本校验”“字段序列化” 等机制的具体实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值