logo

Java序列化与反序列化原理与实践详解

作者:半吊子全栈工匠2026.11.07 02:04浏览量:0

简介:本文系统梳理Java序列化与反序列化的核心概念与典型应用场景,深入剖析Serializable与Externalizable两种实现机制的工作原理与差异,详解transient关键字、自定义序列化、serialVersionUID版本控制等关键细节,并给出接口选型建议与安全实践要点,帮助开发者在对象持久化与网络传输场景中正确、高效、安全地使用序列化技术。

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

在Java世界中,对象存活于JVM的堆内存之中,一旦进程结束或对象失去引用,其状态便随之消亡。而序列化机制正是打破这一限制的关键技术,其定义如下:

  • 序列化:把Java对象转换为字节序列的过程;
  • 反序列化:把字节序列恢复为Java对象的过程。

从本质上讲,序列化是对对象状态的“快照”:它将对象的字段值、类型信息、继承结构等元数据编码为一串有序的字节流。这串字节流独立于JVM进程而存在,可以落盘、可以入网,之后在任意时刻、任意JVM中通过反序列化将其“还原”为一个语义等价的新对象。需要注意的是,反序列化得到的是一个新的对象实例,而非原对象本身,二者只是状态相同。

二、序列化的两大核心用途

对象的序列化主要服务于以下场景:

  1. 持久化对象:把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中,也可以存入数据库的二进制字段,或上传到对象存储等云服务中,使数据在程序生命周期之外长期保存,并在需要时完整恢复。
  2. 网络传输对象:在网络通信中,传输的只能是字节流。分布式系统里的远程调用、跨进程消息投递(例如将消息体写入消息队列)、节点间数据同步等,都需要先把对象序列化,接收端再反序列化还原。

此外,将对象序列化后放入分布式缓存、深拷贝对象等场景,也建立在序列化机制之上。

三、方式一:实现Serializable接口

3.1 基本用法

Serializable是一个标记接口,内部没有定义任何方法,它的作用仅仅是“声明”:告诉JVM这个类允许被序列化。序列化与反序列化分别由ObjectOutputStream的writeObject()和ObjectInputStream的readObject()完成:

  1. class User implements Serializable {
  2. private static final long serialVersionUID = 1L;
  3. private String name;
  4. private transient String password; // 该字段不参与序列化
  5. public User(String name, String password) {
  6. this.name = name;
  7. this.password = password;
  8. }
  9. // 省略getter/setter
  10. }
  11. public class SerializeDemo {
  12. public static void main(String[] args) throws Exception {
  13. // 序列化
  14. try (ObjectOutputStream oos =
  15. new ObjectOutputStream(new FileOutputStream("user.bin"))) {
  16. oos.writeObject(new User("张三", "123456"));
  17. }
  18. // 反序列化
  19. try (ObjectInputStream ois =
  20. new ObjectInputStream(new FileInputStream("user.bin"))) {
  21. User user = (User) ois.readObject();
  22. System.out.println(user.getName()); // 张三
  23. }
  24. }
  25. }

3.2 关键细节剖析

(1)serialVersionUID与版本控制。 若不显式声明该字段,JVM会根据类的结构自动计算生成。类的任何改动(增删字段、修改方法签名等)都可能使其变化,导致旧字节流反序列化时抛出InvalidClassException。因此生产环境应显式声明,以获得可控的版本兼容策略。

(2)递归序列化规则。 序列化对象时,其所有非transient、非static的字段会被递归写出,包括private属性以及其引用的其他对象。因此被引用对象也必须可序列化,否则抛出NotSerializableException。

(3)transient关键字。 用它修饰的字段被完全隔离在序列化机制之外,反序列化后取类型默认值(null、0、false)。典型用途是排除密码等敏感信息、缓存型字段以及不可序列化的资源引用(如连接、线程对象)。

(4)自定义序列化writeObject/readObject。 transient虽简单,但被排除的字段反序列化后无法取回原值。若需更精细的控制,可在类中定义如下私有方法,JVM会通过反射回调它们:

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

借助这对方法,可以控制如何序列化各属性,甚至对某些属性加密序列化或不序列化,兼顾安全与灵活。

(5)构造函数问题。 由于Serializable对象完全以字节流中存储的二进制数据为基础来构造,反序列化过程不会调用任何构造函数,因此该类本身无需默认构造函数。但有一个重要例外:当父类未实现Serializable接口时,反序列化会沿继承链调用最近未序列化父类的无参构造函数来初始化父类状态,此时该父类必须提供可访问的无参构造函数,否则抛出异常。

(6)static字段不参与序列化。 静态字段属于类而非对象实例,序列化只保存对象状态,因此静态字段不会被写入流中。

(7)writeReplace/readResolve。 通过这两个私有方法可在序列化前后替换对象,典型用途是保证单例对象在反序列化后依然唯一。

四、方式二:实现Externalizable接口

Externalizable是Serializable的子接口,但它不是标记接口,而是要求开发者必须实现writeExternal()与readExternal()两个方法,手动决定序列化哪些字段、以什么顺序写入:

  1. class Product implements Externalizable {
  2. private String name;
  3. private double price;
  4. private String secret; // 需要隐藏的字段
  5. public Product() { } // 必须提供public无参构造函数
  6. @Override
  7. public void writeExternal(ObjectOutput out) throws IOException {
  8. out.writeObject(name);
  9. out.writeDouble(price);
  10. // secret不写出,即实现“隐藏”
  11. }
  12. @Override
  13. public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
  14. this.name = (String) in.readObject();
  15. this.price = in.readDouble();
  16. }
  17. }

该方式有两个鲜明特点:

  1. transient在此无效。因为序列化内容完全由writeExternal()/readExternal()的实现决定,修饰符不参与控制,想排除某字段直接不写即可。
  2. 反序列化必须经过构造函数。Externalizable对象反序列化时,会先调用类的public无参构造方法创建空对象,再调用readExternal()填充字段。这与Serializable的默认反序列化方式截然不同。若删除无参构造,或将其访问权限降为private、默认、protected,将抛出java.io.InvalidClassException: no valid constructor异常。因此Externalizable类必须提供public修饰的无参构造函数。

五、两种方式的对比与选型

对比维度 Serializable Externalizable
实现成本 标记接口,零方法实现 必须实现两个方法,繁琐
控制粒度 默认全字段,transient排除,可用writeObject/readObject精细控制 完全手动编排,粒度最细
transient是否有效 有效 无效
构造函数 反序列化不调用构造函数(父类未序列化时需其无参构造) 必须有public无参构造
适用场景 绝大多数业务场景 对序列化格式和性能有极致控制需求时

选型建议:如果只是想隐藏个别属性,例如用户对象user中的密码pwd,选择Serializable并在pwd上加transient即可,简洁高效;若为 Externalizable,就必须把除pwd之外的每个属性都写在writeExternal()里,得不偿失。只有当需要全面接管序列化过程、追求更紧凑的输出格式时,Externalizable才值得投入。

六、实践注意事项

  1. 安全性第一:反序列化不可信来源的字节流存在被构造恶意对象的风险,生产环境应避免反序列化不受信数据,必要时在readObject()中做字段校验。
  2. 版本兼容:显式维护serialVersionUID;类演进时新增字段在反序列化时取默认值,被删除字段的旧数据会被忽略,尽量保持向后兼容。
  3. 跨语言交互:Java原生序列化仅适用于Java体系内部,若系统涉及多语言通信,业界普遍采用JSON等通用数据交换格式替代。
  4. 资源管理:序列化流务必使用try-with-resources确保关闭,防止句柄泄漏;序列化后的字节流可落盘,也可存入对象存储、写入消息队列等通用云服务能力中,用于持久化与跨系统传输。

七、总结

序列化是连接“内存对象”与“字节世界”的桥梁,是持久化与网络传输的基石。掌握Serializable的默认规则与扩展手段、理解Externalizable的构造约束、明确transient的适用边界,再配合版本控制与安全意识,才能真正把序列化机制用对、用好。

发表评论

活动