在 Java 开发中,序列化与反序列化是实现对象持久化、跨进程 / 网络传输的核心技术,无论是分布式系统中的远程调用、对象持久化到文件 / 数据库,还是消息队列的消息传递,都离不开这一基础能力。本文将从核心定义、实现条件、关键关键字、版本号机制到实战案例、常见问题,全方位拆解 Java 序列化与反序列化,让你彻底掌握这一必备知识点。
一、什么是序列化与反序列化?
序列化和反序列化是一对互逆的操作,核心围绕Java 对象与字节序列的转换展开,是实现对象数据跨存储、跨网络传输的基础。
- 序列化 :将内存中的 Java 对象 转换成二进制字节序列的过程。转换后的字节序列可以脱离 JVM 独立存在,支持写入文件、存入数据库、通过网络传输到其他服务器等场景。
- 反序列化 :将二进制字节序列 恢复成Java 对象的过程。通过反序列化,可在其他 JVM 进程、其他服务器中重建与原对象数据一致的实例,恢复对象的属性和状态。
核心应用场景
- 对象持久化:将对象保存到文件、数据库(如 Redis 的对象存储),程序重启后可通过反序列化恢复对象状态;
- 跨进程 / 网络传输:分布式系统中,远程方法调用(如 RMI)、微服务间接口调用、消息队列(如 RocketMQ/Kafka)的消息传递,都会将对象序列化为字节序列后传输;
- 缓存存储:将复杂对象序列化后存入缓存,避免重复创建对象带来的性能开销。
简单来说,序列化解决了Java 对象在不同环境、不同进程间的传输和持久化问题,让对象可以 "脱离 JVM 存活"。
二、Java 实现序列化的完整条件
Java 序列化是基于接口标记的轻量级实现,无需实现任何方法,核心只需满足一个基础条件,同时可通过自定义方法扩展序列化逻辑。
基础条件:实现 Serializable 标记接口
要让一个 Java 类的对象支持序列化,该类必须实现java.io.Serializable接口,这是一个标记接口(无任何抽象方法),仅用于告诉 JVM:该类的对象可以被序列化机制处理。
import java.io.Serializable;
// 实现Serializable接口,支持序列化
public class User implements Serializable {
private String username;
private Integer age;
private String password;
// 无参构造、有参构造、get/set方法
public User() {}
public User(String username, Integer age, String password) {
this.username = username;
this.age = age;
this.password = password;
}
@Override
public String toString() {
return "User{username='" + username + "', age=" + age + ", password='" + password + "'}";
}
// 省略get/set方法
}
注意 :如果一个类实现了Serializable,其所有成员变量 也必须支持序列化(要么是基本数据类型,要么是实现了Serializable的引用类型),否则会抛出NotSerializableException异常。
扩展:自定义序列化 / 反序列化逻辑
默认情况下,JVM 会自动完成对象的序列化 / 反序列化,按类的成员变量顺序依次读写。若需要自定义逻辑(如屏蔽敏感字段、对数据加密 / 解密),可在类中手动定义以下两个私有方法(JVM 会通过反射自动识别,无需实现任何接口):
private void writeObject(ObjectOutputStream out) throws IOException:自定义序列化逻辑,替代默认的序列化过程;private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException:自定义反序列化逻辑,替代默认的反序列化过程。
示例:对密码字段加密序列化,反序列化时解密
private void writeObject(ObjectOutputStream out) throws IOException {
// 先序列化默认的成员变量(username、age)
out.defaultWriteObject();
// 对密码加密后序列化(简单示例:反转字符串,实际开发用对称/非对称加密)
String encryptPwd = new StringBuffer(this.password).reverse().toString();
out.writeObject(encryptPwd);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 先反序列化默认的成员变量
in.defaultReadObject();
// 解密密码并赋值
String encryptPwd = (String) in.readObject();
this.password = new StringBuffer(encryptPwd).reverse().toString();
}
三、transient 关键字:屏蔽不需要序列化的字段
在实际开发中,有些字段无需序列化(如临时状态字段、敏感字段、由其他字段推导的派生字段),此时可使用transient关键字修饰该字段,被修饰的字段不会被序列化 ,反序列化时会被赋值为默认值(基本类型为默认值,引用类型为 null)。
核心特性
- 仅作用于序列化 :
transient仅影响对象的序列化过程,不影响对象在内存中的正常使用; - 默认值规则:反序列化时,transient 字段会被重置为默认值,而非原对象的取值;
- 不影响静态字段 :静态字段属于类,而非对象,序列化仅处理对象的实例数据,因此静态字段无需用
transient修饰,也不会被序列化。
实战示例:屏蔽密码字段的序列化
修改上述 User 类,用transient修饰 password 字段,避免敏感密码被序列化:
public class User implements Serializable {
private String username;
private Integer age;
private transient String password; // 屏蔽序列化
// 构造方法、toString、get/set方法不变
}
测试序列化与反序列化:
import java.io.*;
public class SerializeTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 1. 创建对象
User user = new User("张三", 25, "123456");
System.out.println("序列化前:" + user); // 序列化前:User{username='张三', age=25, password='123456'}
// 2. 序列化到文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"));
oos.writeObject(user);
oos.close();
// 3. 从文件反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"));
User desUser = (User) ois.readObject();
ois.close();
System.out.println("序列化后:" + desUser); // 序列化后:User{username='张三', age=25, password='null'}
}
}
可见,被transient修饰的 password 字段,反序列化后变为 null,实现了敏感字段的屏蔽。
四、serialVersionUID:序列化的版本号机制
serialVersionUID是序列化的核心版本标识 ,用于标记序列化类的版本,保证序列化对象与反序列化类的版本一致性,是解决反序列化失败的关键。
1. 核心作用
当一个类实现了Serializable接口后,JVM 会在序列化时将该类的serialVersionUID写入字节序列;反序列化时,会将字节序列中的serialVersionUID与当前类的serialVersionUID对比:
- 一致:版本匹配,正常反序列化;
- 不一致 :版本不匹配,抛出
InvalidClassException异常,反序列化失败。
2. 生成方式:显式指定 vs 隐式生成
2.1 隐式生成(不推荐)
若类中未显式定义serialVersionUID,JVM 会根据类的结构信息 (类名、成员变量、方法、访问修饰符等)自动计算一个哈希值作为serialVersionUID。问题 :只要类的结构发生微小修改(如新增 / 删除一个成员变量、修改方法名、甚至加一个注释),JVM 重新计算的serialVersionUID就会发生变化,导致原有序列化的对象无法反序列化,引发线上问题。
2.2 显式指定(推荐)
在类中手动定义serialVersionUID,通常为private static final long类型,固定值即可,避免 JVM 自动计算带来的版本不一致问题。生成技巧 :IDEA/Eclipse 都有自动生成serialVersionUID的功能,快捷键可直接生成唯一的长整型值,也可手动指定(如 1L、100L)。
3. 实战示例:显式指定 serialVersionUID
修改 User 类,添加显式的serialVersionUID:
public class User implements Serializable {
// 显式指定序列化版本号,固定不变
private static final long serialVersionUID = 1L;
private String username;
private Integer age;
private transient String password;
// 构造方法、toString、get/set方法不变
}
此时,即便后续对 User 类做小修改(如新增一个gender字段),只要serialVersionUID保持 1L 不变,原有序列化的对象仍可正常反序列化(新增的字段会被赋值为默认值)。
4. 常见场景:类结构修改后如何保证反序列化兼容?
- 新增成员变量:反序列化时,新增字段会被赋值为默认值,不影响原有字段的反序列化;
- 删除成员变量:反序列化时,字节序列中的原有字段会被忽略,不影响现有字段的反序列化;
- 修改成员变量名:视为删除原字段 + 新增新字段,原字段值丢失,新字段为默认值(需避免);
- 修改成员变量类型 :会导致
InvalidClassException(需避免,若必须修改,需保证版本号一致并做兼容处理)。
核心原则 :类结构修改后,只要显式指定的 serialVersionUID 不变,且不修改原有字段的类型 / 名称,就能保证反序列化的向前兼容。
五、序列化与反序列化的核心注意事项
1. 序列化仅处理对象的实例数据,不处理静态字段
静态字段属于类级别的数据,存储在方法区,而非对象的堆内存中,序列化仅针对堆中的实例数据,因此静态字段不会被序列化,反序列化时会取当前类的静态字段值,而非序列化时的取值。
2. 父类的序列化规则
- 若父类实现了
Serializable,则子类会继承父类的序列化能力,所有父类成员变量都会被序列化; - 若父类未实现
Serializable,则父类必须有无参构造方法 ,否则反序列化时会抛出InstantiationException异常。因为此时子类序列化时,仅序列化自身成员变量,反序列化时会通过父类无参构造创建父类实例,再初始化子类成员变量。
3. 序列化对象的引用类型成员必须支持序列化
若一个类的成员变量是引用类型(如自定义的 Address 类),则该引用类型必须也实现Serializable接口,否则序列化时会抛出NotSerializableException异常。
4. 反序列化时,类必须存在且可访问
反序列化的前提是:目标类在当前 JVM 中存在 ,且具有可访问的构造方法(无参构造),否则会抛出ClassNotFoundException或InstantiationException异常。
5. 序列化不保证线程安全
Java 的序列化相关类(ObjectOutputStream、ObjectInputStream)不是线程安全的,若多线程并发进行序列化 / 反序列化操作,需要手动加锁保证线程安全。
6. 避免序列化不可变对象的可变引用
若对象中包含不可变对象(如 String)的可变引用,序列化时会保存引用地址,反序列化后可能导致引用泄露,建议使用transient修饰并在反序列化时重新初始化。
六、序列化的替代方案
虽然 Java 原生序列化使用简单,但存在字节序列体积大、序列化效率低、跨语言兼容性差等问题,在分布式系统、微服务等场景中,通常会使用更高效的序列化框架替代,常见的有:
- JSON 序列化:如 FastJSON、Jackson、Gson,将对象转为 JSON 字符串,跨语言兼容性好、可读性高,适合轻量级的网络传输,缺点是性能一般,无法保存对象的全状态(如静态字段、transient 字段);
- Protobuf:谷歌开源的二进制序列化框架,序列化后字节序列体积小、效率高、跨语言,适合高性能的分布式系统、消息队列,缺点是需要定义.proto 文件,有一定的学习成本;
- Hessian:轻量级的二进制序列化框架,支持跨语言,序列化效率高于 Java 原生,适合远程方法调用(如 Dubbo 默认使用 Hessian 序列化);
- Kryo:高性能的 Java 二进制序列化框架,效率远高于 Java 原生,适合大数据、缓存等场景,缺点是跨语言兼容性一般。
选型建议:
- 快速开发、跨语言轻量传输:选择 JSON 序列化(Jackson);
- 分布式系统、高性能传输:选择 Protobuf 或 Hessian;
- Java 专属、大数据 / 缓存:选择 Kryo;
- 简单本地持久化、无跨语言需求:使用 Java 原生序列化。
七、总结
Java 序列化与反序列化是实现对象持久化和跨进程传输的基础,核心围绕Serializable标记接口展开,关键知识点可总结为 "一个接口、一个关键字、一个版本号":
- 一个接口 :
Serializable,标记类支持序列化,无抽象方法; - 一个关键字 :
transient,屏蔽不需要序列化的字段,反序列化后为默认值; - 一个版本号 :
serialVersionUID,显式指定保证版本一致性,避免反序列化失败。
同时,开发中需注意父类序列化规则、静态字段不序列化、引用类型成员需支持序列化等细节,避免出现NotSerializableException、InvalidClassException等异常。在高性能、跨语言的场景中,可替代为 Protobuf、Hessian 等更高效的序列化框架,兼顾性能和兼容性。
掌握序列化与反序列化的核心原理和使用规范,不仅能解决实际开发中的对象传输和持久化问题,也是 Java 面试中的高频考点,希望本文能帮助你彻底吃透这一核心知识点。