Java序列化与反序列化机制详解

作者:demo2026.11.07 02:21浏览量:0

简介:本文系统讲解Java序列化与反序列化的核心机制,重点剖析Serializable与Externalizable两大接口的工作原理、差异及适用场景。读者将掌握transient关键字的作用边界、writeObject/readObject的自定义控制方法、父类构造函数调用规则,以及Externalizable对默认构造函数的强制要求,从而在实际开发中规避常见异常并做出合理的技术选型。

Java序列化与反序列化机制详解

一、概念界定:什么是序列化与反序列化

在Java程序中,对象是存放在堆内存中的运行时实体,其生命周期通常局限于当前JVM进程。一旦进程结束或需要跨网络传输,对象本身无法直接”搬运”。序列化(Serialization)就是将一个对象的状态转换为可存储或可传输的字节序列的过程;反序列化(Deserialization)则是将这些字节序列重新还原为内存中对象的过程。

这一机制的价值体现在多个场景:将对象状态持久化到磁盘文件、通过消息队列或RPC框架跨进程传输对象、在分布式系统中缓存会话数据、以及深拷贝对象等。理解其底层规则,是写出健壮持久化与通信代码的前提。

二、实现序列化的两条路径

在Java中,一个类的对象若要支持序列化,其类必须实现以下两个接口之一:

  • java.io.Serializable 接口
  • java.io.Externalizable 接口

两者并非并列关系:Externalizable 继承自 Serializable,是对序列化过程的进一步定制。下面分别剖析。

三、Serializable 接口:标记式序列化

3.1 基本工作方式

Serializable 是一个不含任何方法的标记接口(marker interface)。类实现该接口或它的子接口后,即向序列化机制声明”本类对象允许被序列化”。序列化时,机制会递归地处理对象的所有属性——包括 private 字段,以及该对象所引用的其他对象(被引用对象同样需要可序列化,否则抛出 NotSerializableException)。

  1. public class Order implements Serializable {
  2. private static final long serialVersionUID = 1L;
  3. private String orderId;
  4. private BigDecimal amount;
  5. private transient String sessionToken; // 不参与序列化
  6. // getter/setter 省略
  7. }

3.2 transient 关键字的作用与局限

使用 transient 修饰的字段会被排除在序列化之外,反序列化后该字段恢复为默认值(对象为 null,基本类型为 0/false)。这种方式简单直接,但存在一个明显短板:被 transient 隔离的字段在反序列化时完全无法恢复,如果该字段本可以通过其他字段推导或需要特殊初始化,就无能为力。

3.3 自定义序列化:writeObject 与 readObject

为获得更细粒度的控制,可以在类中定义以下两个私有方法:

  1. private void writeObject(ObjectOutputStream out) throws IOException {
  2. out.defaultWriteObject(); // 先序列化非 transient 字段
  3. out.writeObject(encrypt(sessionToken)); // 再对敏感字段加密后写入
  4. }
  5. private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
  6. in.defaultReadObject();
  7. this.sessionToken = decrypt((String) in.readObject());
  8. }

通过这对方法,开发者可以决定序列化哪些属性、以何种顺序写入、是否加密、是否做字段兼容性转换,甚至完全跳过某些属性。这比单纯使用 transient 灵活得多,是处理敏感数据与版本兼容的常用手段。

3.4 构造函数调用规则

Serializable 对象的反序列化并不通过构造函数完成,而是直接依据字节流中保存的二进制状态重建对象。因此:

  • 实现 Serializable 的类不需要提供默认无参构造函数;
  • 但如果其父类没有实现 Serializable,反序列化时会调用父类的默认构造函数来初始化父类部分的状态。此时父类必须存在可访问的无参构造函数,否则抛出异常。

这一规则常被忽视:一个可序列化子类继承自不可序列化父类时,父类字段的状态不会被序列化保存,反序列化后父类字段将回到构造函数初始化的值。

3.5 serialVersionUID 的意义

建议显式声明 serialVersionUID。它用于版本校验:序列化与反序列化双方该值不一致时,会抛出 InvalidClassException。显式指定可避免因类结构微调(如新增方法)导致编译器生成的UID变化,从而破坏已有数据的兼容性。

四、Externalizable 接口:完全受控的序列化

4.1 接口构成

Externalizable 继承自 Serializable,并要求实现两个方法:

  1. public class Config implements Externalizable {
  2. private String host;
  3. private int port;
  4. public Config() {} // 必须提供 public 无参构造
  5. @Override
  6. public void writeExternal(ObjectOutput out) throws IOException {
  7. out.writeUTF(host);
  8. out.writeInt(port);
  9. }
  10. @Override
  11. public void readExternal(ObjectInput in) throws IOException {
  12. this.host = in.readUTF();
  13. this.port = in.readInt();
  14. }
  15. }

4.2 关键差异

  • 完全由开发者决定序列化内容:写入与读取的字段、顺序、格式全部手动控制,transient 在此无效(因为根本不走默认字段序列化流程)。
  • 反序列化先调用 public 无参构造:与 Serializable 不同,Externalizable 对象在反序列化时会先通过无参构造函数创建实例,再调用 readExternal 填充状态。因此该类必须有 public 的无参构造函数;若删除该构造函数,或将其访问权限设为 private、默认(包级)或 protected,反序列化时会抛出 InvalidClassException: no valid constructor。
  • 性能与体积:由于跳过了反射式的字段遍历与元数据写入,Externalizable 通常能获得更紧凑的字节流和更快的处理速度,适合对传输体积敏感的场景。

4.3 适用场景对比

维度 Serializable Externalizable
实现成本 低,标记即可 高,需手写读写逻辑
控制粒度 中(transient + 自定义方法) 完全可控
构造函数要求 自身不要求,父类可能要求 自身必须有 public 无参构造
版本兼容 依赖 serialVersionUID 由开发者自行保证读写顺序一致
性能 一般 通常更优

五、实践中的注意事项

  1. 被引用对象必须可序列化:深层引用链中任一环节不可序列化,都会导致整体失败,且异常信息往往只指向最外层,排查时需逐层检查。
  2. 静态字段不参与序列化:序列化保存的是实例状态,static 字段属于类级别,不会被写入字节流。
  3. 安全风险:反序列化不可信字节流可能触发恶意构造,应避免对来源不明的数据进行反序列化,必要时使用白名单校验类类型。
  4. 单例与序列化:默认反序列化会创建新实例,破坏单例约定,可通过 readResolve() 方法返回既有实例。
  5. 选型建议:一般业务对象优先使用 Serializable 配合 serialVersionUID;对体积、性能或字段控制有强需求时,再考虑 Externalizable。

六、小结

序列化与反序列化是Java对象跨越内存边界的基础能力。Serializable 以标记接口加默认字段遍历的方式提供开箱即用的支持,并通过 transient 与 writeObject/readObject 提供有限定制;Externalizable 则将控制权完全交给开发者,代价是必须手写读写逻辑并保证 public 无参构造函数的存在。理解两者在构造函数调用、字段控制与兼容性上的差异,才能在实际工程中做出恰当选择,避免反序列化异常与数据不一致问题。