简介:本文系统讲解Java序列化与反序列化的核心机制,重点剖析Serializable与Externalizable两大接口的工作原理、差异及适用场景。读者将掌握transient关键字的作用边界、writeObject/readObject的自定义控制方法、父类构造函数调用规则,以及Externalizable对默认构造函数的强制要求,从而在实际开发中规避常见异常并做出合理的技术选型。
在Java程序中,对象是存放在堆内存中的运行时实体,其生命周期通常局限于当前JVM进程。一旦进程结束或需要跨网络传输,对象本身无法直接”搬运”。序列化(Serialization)就是将一个对象的状态转换为可存储或可传输的字节序列的过程;反序列化(Deserialization)则是将这些字节序列重新还原为内存中对象的过程。
这一机制的价值体现在多个场景:将对象状态持久化到磁盘文件、通过消息队列或RPC框架跨进程传输对象、在分布式系统中缓存会话数据、以及深拷贝对象等。理解其底层规则,是写出健壮持久化与通信代码的前提。
在Java中,一个类的对象若要支持序列化,其类必须实现以下两个接口之一:
java.io.Serializable 接口java.io.Externalizable 接口两者并非并列关系:Externalizable 继承自 Serializable,是对序列化过程的进一步定制。下面分别剖析。
Serializable 是一个不含任何方法的标记接口(marker interface)。类实现该接口或它的子接口后,即向序列化机制声明”本类对象允许被序列化”。序列化时,机制会递归地处理对象的所有属性——包括 private 字段,以及该对象所引用的其他对象(被引用对象同样需要可序列化,否则抛出 NotSerializableException)。
public class Order implements Serializable {private static final long serialVersionUID = 1L;private String orderId;private BigDecimal amount;private transient String sessionToken; // 不参与序列化// getter/setter 省略}
使用 transient 修饰的字段会被排除在序列化之外,反序列化后该字段恢复为默认值(对象为 null,基本类型为 0/false)。这种方式简单直接,但存在一个明显短板:被 transient 隔离的字段在反序列化时完全无法恢复,如果该字段本可以通过其他字段推导或需要特殊初始化,就无能为力。
为获得更细粒度的控制,可以在类中定义以下两个私有方法:
private void writeObject(ObjectOutputStream out) throws IOException {out.defaultWriteObject(); // 先序列化非 transient 字段out.writeObject(encrypt(sessionToken)); // 再对敏感字段加密后写入}private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {in.defaultReadObject();this.sessionToken = decrypt((String) in.readObject());}
通过这对方法,开发者可以决定序列化哪些属性、以何种顺序写入、是否加密、是否做字段兼容性转换,甚至完全跳过某些属性。这比单纯使用 transient 灵活得多,是处理敏感数据与版本兼容的常用手段。
Serializable 对象的反序列化并不通过构造函数完成,而是直接依据字节流中保存的二进制状态重建对象。因此:
Serializable 的类不需要提供默认无参构造函数;Serializable,反序列化时会调用父类的默认构造函数来初始化父类部分的状态。此时父类必须存在可访问的无参构造函数,否则抛出异常。这一规则常被忽视:一个可序列化子类继承自不可序列化父类时,父类字段的状态不会被序列化保存,反序列化后父类字段将回到构造函数初始化的值。
建议显式声明 serialVersionUID。它用于版本校验:序列化与反序列化双方该值不一致时,会抛出 InvalidClassException。显式指定可避免因类结构微调(如新增方法)导致编译器生成的UID变化,从而破坏已有数据的兼容性。
Externalizable 继承自 Serializable,并要求实现两个方法:
public class Config implements Externalizable {private String host;private int port;public Config() {} // 必须提供 public 无参构造@Overridepublic void writeExternal(ObjectOutput out) throws IOException {out.writeUTF(host);out.writeInt(port);}@Overridepublic void readExternal(ObjectInput in) throws IOException {this.host = in.readUTF();this.port = in.readInt();}}
transient 在此无效(因为根本不走默认字段序列化流程)。Serializable 不同,Externalizable 对象在反序列化时会先通过无参构造函数创建实例,再调用 readExternal 填充状态。因此该类必须有 public 的无参构造函数;若删除该构造函数,或将其访问权限设为 private、默认(包级)或 protected,反序列化时会抛出 InvalidClassException: no valid constructor。Externalizable 通常能获得更紧凑的字节流和更快的处理速度,适合对传输体积敏感的场景。| 维度 | Serializable | Externalizable |
|---|---|---|
| 实现成本 | 低,标记即可 | 高,需手写读写逻辑 |
| 控制粒度 | 中(transient + 自定义方法) | 完全可控 |
| 构造函数要求 | 自身不要求,父类可能要求 | 自身必须有 public 无参构造 |
| 版本兼容 | 依赖 serialVersionUID | 由开发者自行保证读写顺序一致 |
| 性能 | 一般 | 通常更优 |
static 字段属于类级别,不会被写入字节流。readResolve() 方法返回既有实例。Serializable 配合 serialVersionUID;对体积、性能或字段控制有强需求时,再考虑 Externalizable。序列化与反序列化是Java对象跨越内存边界的基础能力。Serializable 以标记接口加默认字段遍历的方式提供开箱即用的支持,并通过 transient 与 writeObject/readObject 提供有限定制;Externalizable 则将控制权完全交给开发者,代价是必须手写读写逻辑并保证 public 无参构造函数的存在。理解两者在构造函数调用、字段控制与兼容性上的差异,才能在实际工程中做出恰当选择,避免反序列化异常与数据不一致问题。