一文搞懂Java序列化与反序列化机制

作者:4042026.11.07 02:29浏览量:0

简介:本文系统讲解Java序列化与反序列化的核心概念与底层规则,深入剖析Serializable与Externalizable两套机制的差异、transient关键字的作用边界、构造函数的调用规则以及自定义序列化技巧,并结合典型代码示例给出选型建议与常见陷阱规避方法,帮助开发者在对象持久化、网络传输等场景中写出更健壮的代码。

一、什么是序列化与反序列化

在Java中,序列化(Serialization)是指将内存中的对象转换为字节序列的过程;反序列化(Deserialization)则是与之相反的过程,即把字节序列重新恢复为Java对象。

一个对象只有变成字节流,才能脱离JVM内存的束缚,被写入磁盘、存入缓存、或者在网络中从一台机器传送到另一台机器。因此,序列化机制主要服务于以下几类场景:

  • 网络传输对象:在RPC调用、分布式通信中,对象需要以字节流的形式在节点之间传递;
  • 持久化存储:将对象状态保存到文件、数据库或缓存中,之后可以原样恢复;
  • 深拷贝:通过“序列化再反序列化”的方式,快速得到一个与原对象完全独立的新对象;
  • 跨进程传递:在不同进程或不同JVM实例之间交换对象数据。

二、Serializable 接口:最常用的序列化方式

1. 基本用法

一个类只要实现了 Serializable 接口,它的实例就具备了被序列化的能力。这是一个标记接口,本身不含任何方法,实现成本极低:

  1. public class User implements Serializable {
  2. private static final long serialVersionUID = 1L;
  3. private String name;
  4. private int age;
  5. private transient String password; // 该字段不参与序列化
  6. // getter/setter 省略
  7. }

一旦类实现了该接口,它的所有属性都会被纳入序列化范围——包括 private 私有字段,也包括属性所引用的其他对象(前提是这些被引用的对象的类同样实现了 Serializable,否则会抛出 NotSerializableException)。如果不希望某个字段被序列化,用 transient 修饰即可。

具体的读写操作通过 ObjectInputStream 与 ObjectOutputStream 完成:

  1. // 序列化:对象 -> 字节流
  2. try (ObjectOutputStream oos =
  3. new ObjectOutputStream(new FileOutputStream("user.bin"))) {
  4. oos.writeObject(user);
  5. }
  6. // 反序列化:字节流 -> 对象
  7. try (ObjectInputStream ois =
  8. new ObjectInputStream(new FileInputStream("user.bin"))) {
  9. User u = (User) ois.readObject();
  10. }

2. 构造函数的调用规则

这是许多开发者容易忽略的细节。由于反序列化时对象完全依据字节流中存储的二进制数据来重建,并不走正常的对象创建流程,因此:

  • 反序列化过程不会调用序列化类自身的任何构造函数,所以该类即便没有无参构造函数也不影响反序列化;
  • 但如果父类没有实现 Serializable 接口,JVM 就无法通过序列化机制恢复父类部分的状态,只能像普通对象一样初始化父类,此时会调用父类的无参构造函数。因此这种情况下父类必须提供可访问的默认构造函数,否则反序列化会直接失败。

3. serialVersionUID 的作用

建议在每个可序列化类中显式声明 serialVersionUID。它是类的“版本号”,反序列化时会校验字节流中的版本号与当前类是否一致,不一致则抛出 InvalidClassException。若不显式声明,编译器会根据类结构自动生成,类的任何微小改动(如新增字段)都可能导致版本号变化,使旧数据无法反序列化。

4. transient 的能力边界

使用 transient 阻止字段序列化虽然简单方便,但它是一种“全有或全无”的粗暴手段:被修饰的字段被完全隔离在序列化机制之外,反序列化后该字段只会是类型默认值(如 null、0),原始值彻底丢失。

如果需要更精细的控制,可以在类中提供 writeObject() 与 readObject() 这两个私有方法,JVM 会通过反射自动回调它们:

  1. private void writeObject(ObjectOutputStream out) throws IOException {
  2. out.defaultWriteObject(); // 先按默认规则序列化其他字段
  3. out.writeObject(encrypt(password)); // 敏感字段加密后再写出
  4. }
  5. private void readObject(ObjectInputStream in)
  6. throws IOException, ClassNotFoundException {
  7. in.defaultReadObject();
  8. password = decrypt((String) in.readObject()); // 读入后解密还原
  9. }

借助这两个方法,开发者可以决定各字段如何序列化:可以完全不写出某些字段,也可以对敏感字段做加密处理,实现“既保护数据又不丢数据”的效果。

三、Externalizable 接口:完全手动控制

Externalizable 是 Serializable 的子接口,但它不是标记接口,要求开发者手动实现 writeExternal() 和 readExternal() 两个方法,自行决定哪些属性以何种方式写出和读回:

  1. public class User implements Externalizable {
  2. private String name;
  3. private String password;
  4. public User() { } // 必须保留 public 无参构造函数
  5. @Override
  6. public void writeExternal(ObjectOutput out) throws IOException {
  7. out.writeObject(name);
  8. // 密码不写出,或只写出加密后的值
  9. }
  10. @Override
  11. public void readExternal(ObjectInput in)
  12. throws IOException, ClassNotFoundException {
  13. name = (String) in.readObject();
  14. }
  15. }

它有两个与 Serializable 截然不同的关键规则:

  1. transient 在这里完全无效。因为哪些字段被序列化完全由 writeExternal()/readExternal() 的实现代码决定,修饰符不起任何作用;
  2. 反序列化时会先调用类的无参构造方法来创建空对象,再通过 readExternal() 填充字段。这是它与默认反序列化机制的本质区别。因此 Externalizable 类必须提供 public 修饰的无参构造函数——如果把无参构造删除,或将其访问权限降为 private、默认、protected,反序列化时会抛出 InvalidClassException: no valid constructor 异常。

四、两种方式如何选型

对比维度 Serializable Externalizable
实现成本 低,标记接口即可 高,需手写读写方法
控制粒度 默认全量,可用 transient 排除 完全由代码控制
transient 有效 无效
构造函数要求 本类无需;父类未实现接口时需默认构造 必须 public 无参构造

实际开发中的选型建议:

  • 只想隐藏个别字段(例如用户对象中的密码字段):直接使用 Serializable,在目标字段前加 transient 即可,无需为其余每个字段手写读写逻辑,简单高效;
  • 需要精确控制序列化格式、压缩数据量或实现复杂编解码逻辑:选择 Externalizable,虽然代码量更大,但灵活性最高。

五、常见陷阱与最佳实践

  1. 静态变量不会被序列化。序列化针对的是对象状态,而静态成员属于类,不随对象写入字节流;
  2. 敏感信息务必处理。密码、密钥等字段要么加 transient,要么通过 writeObject()/readObject() 加密传输,避免字节流被截获后直接还原出明文;
  3. 单例模式需防御序列化破坏。反序列化会绕过构造函数新造对象,可在单例类中添加 readResolve() 方法返回唯一实例,保证单例语义不被破坏;
  4. 显式声明 serialVersionUID,避免类结构演进后旧数据无法兼容;
  5. 引用对象也要可序列化。对象图中任何一个节点未实现 Serializable,整个序列化都会失败,设计类结构时需通盘考虑。

六、总结

序列化与反序列化是Java对象走向“可传输、可存储”状态的基础能力。Serializable 胜在简单,配合 transient 与 writeObject()/readObject() 可以覆盖绝大多数日常需求;Externalizable 胜在完全可控,适合对格式和性能有严格要求的场景。理解两者的构造函数调用差异、transient 的作用范围以及版本号机制,是避开序列化陷阱、写出健壮代码的关键。在实际项目中,还应结合安全性、兼容性与可维护性综合权衡,选择最适合当前业务的序列化方案。