Java序列化和反序列化

在 Java 开发中,序列化与反序列化是实现对象持久化、跨进程 / 网络传输的核心技术,无论是分布式系统中的远程调用、对象持久化到文件 / 数据库,还是消息队列的消息传递,都离不开这一基础能力。本文将从核心定义、实现条件、关键关键字、版本号机制到实战案例、常见问题,全方位拆解 Java 序列化与反序列化,让你彻底掌握这一必备知识点。

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

序列化和反序列化是一对互逆的操作,核心围绕Java 对象与字节序列的转换展开,是实现对象数据跨存储、跨网络传输的基础。

  • 序列化 :将内存中的 Java 对象 转换成二进制字节序列的过程。转换后的字节序列可以脱离 JVM 独立存在,支持写入文件、存入数据库、通过网络传输到其他服务器等场景。
  • 反序列化 :将二进制字节序列 恢复成Java 对象的过程。通过反序列化,可在其他 JVM 进程、其他服务器中重建与原对象数据一致的实例,恢复对象的属性和状态。

核心应用场景

  1. 对象持久化:将对象保存到文件、数据库(如 Redis 的对象存储),程序重启后可通过反序列化恢复对象状态;
  2. 跨进程 / 网络传输:分布式系统中,远程方法调用(如 RMI)、微服务间接口调用、消息队列(如 RocketMQ/Kafka)的消息传递,都会将对象序列化为字节序列后传输;
  3. 缓存存储:将复杂对象序列化后存入缓存,避免重复创建对象带来的性能开销。

简单来说,序列化解决了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 会通过反射自动识别,无需实现任何接口):

  1. private void writeObject(ObjectOutputStream out) throws IOException:自定义序列化逻辑,替代默认的序列化过程;
  2. 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)。

核心特性

  1. 仅作用于序列化transient仅影响对象的序列化过程,不影响对象在内存中的正常使用;
  2. 默认值规则:反序列化时,transient 字段会被重置为默认值,而非原对象的取值;
  3. 不影响静态字段 :静态字段属于类,而非对象,序列化仅处理对象的实例数据,因此静态字段无需用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. 常见场景:类结构修改后如何保证反序列化兼容?

  1. 新增成员变量:反序列化时,新增字段会被赋值为默认值,不影响原有字段的反序列化;
  2. 删除成员变量:反序列化时,字节序列中的原有字段会被忽略,不影响现有字段的反序列化;
  3. 修改成员变量名:视为删除原字段 + 新增新字段,原字段值丢失,新字段为默认值(需避免);
  4. 修改成员变量类型 :会导致InvalidClassException(需避免,若必须修改,需保证版本号一致并做兼容处理)。

核心原则 :类结构修改后,只要显式指定的 serialVersionUID 不变,且不修改原有字段的类型 / 名称,就能保证反序列化的向前兼容。

五、序列化与反序列化的核心注意事项

1. 序列化仅处理对象的实例数据,不处理静态字段

静态字段属于类级别的数据,存储在方法区,而非对象的堆内存中,序列化仅针对堆中的实例数据,因此静态字段不会被序列化,反序列化时会取当前类的静态字段值,而非序列化时的取值。

2. 父类的序列化规则

  • 若父类实现了Serializable,则子类会继承父类的序列化能力,所有父类成员变量都会被序列化;
  • 若父类未实现Serializable,则父类必须有无参构造方法 ,否则反序列化时会抛出InstantiationException异常。因为此时子类序列化时,仅序列化自身成员变量,反序列化时会通过父类无参构造创建父类实例,再初始化子类成员变量。

3. 序列化对象的引用类型成员必须支持序列化

若一个类的成员变量是引用类型(如自定义的 Address 类),则该引用类型必须也实现Serializable接口,否则序列化时会抛出NotSerializableException异常。

4. 反序列化时,类必须存在且可访问

反序列化的前提是:目标类在当前 JVM 中存在 ,且具有可访问的构造方法(无参构造),否则会抛出ClassNotFoundExceptionInstantiationException异常。

5. 序列化不保证线程安全

Java 的序列化相关类(ObjectOutputStreamObjectInputStream)不是线程安全的,若多线程并发进行序列化 / 反序列化操作,需要手动加锁保证线程安全。

6. 避免序列化不可变对象的可变引用

若对象中包含不可变对象(如 String)的可变引用,序列化时会保存引用地址,反序列化后可能导致引用泄露,建议使用transient修饰并在反序列化时重新初始化。

六、序列化的替代方案

虽然 Java 原生序列化使用简单,但存在字节序列体积大、序列化效率低、跨语言兼容性差等问题,在分布式系统、微服务等场景中,通常会使用更高效的序列化框架替代,常见的有:

  1. JSON 序列化:如 FastJSON、Jackson、Gson,将对象转为 JSON 字符串,跨语言兼容性好、可读性高,适合轻量级的网络传输,缺点是性能一般,无法保存对象的全状态(如静态字段、transient 字段);
  2. Protobuf:谷歌开源的二进制序列化框架,序列化后字节序列体积小、效率高、跨语言,适合高性能的分布式系统、消息队列,缺点是需要定义.proto 文件,有一定的学习成本;
  3. Hessian:轻量级的二进制序列化框架,支持跨语言,序列化效率高于 Java 原生,适合远程方法调用(如 Dubbo 默认使用 Hessian 序列化);
  4. Kryo:高性能的 Java 二进制序列化框架,效率远高于 Java 原生,适合大数据、缓存等场景,缺点是跨语言兼容性一般。

选型建议

  • 快速开发、跨语言轻量传输:选择 JSON 序列化(Jackson);
  • 分布式系统、高性能传输:选择 Protobuf 或 Hessian;
  • Java 专属、大数据 / 缓存:选择 Kryo;
  • 简单本地持久化、无跨语言需求:使用 Java 原生序列化。

七、总结

Java 序列化与反序列化是实现对象持久化和跨进程传输的基础,核心围绕Serializable标记接口展开,关键知识点可总结为 "一个接口、一个关键字、一个版本号":

  1. 一个接口Serializable,标记类支持序列化,无抽象方法;
  2. 一个关键字transient,屏蔽不需要序列化的字段,反序列化后为默认值;
  3. 一个版本号serialVersionUID,显式指定保证版本一致性,避免反序列化失败。

同时,开发中需注意父类序列化规则、静态字段不序列化、引用类型成员需支持序列化等细节,避免出现NotSerializableExceptionInvalidClassException等异常。在高性能、跨语言的场景中,可替代为 Protobuf、Hessian 等更高效的序列化框架,兼顾性能和兼容性。

掌握序列化与反序列化的核心原理和使用规范,不仅能解决实际开发中的对象传输和持久化问题,也是 Java 面试中的高频考点,希望本文能帮助你彻底吃透这一核心知识点。

相关推荐
文公子WGZ2 小时前
将java 21切换成java 25
java·开发语言
AI精钢2 小时前
OpenLobster 的优势与劣势:一次面向 OpenClaw 用户的架构审视
java·微服务·架构·ai agent·mcp·openclaw·openlobster
MonkeyKing_sunyuhua2 小时前
本地将镜像打包推送到阿里云的镜像服务器
java·服务器·阿里云
飞Link2 小时前
Kafka~本地Python Kafka发送数据,服务端Kafka消费不到
java·分布式·kafka
喵喵蒻葉睦2 小时前
力扣 hot100 滑动窗口最大值 单调双端队列 java 简单题解
java·数据结构·算法·leetcode·双端队列·滑动窗口·队列
2401_831920742 小时前
C++与Qt图形开发
开发语言·c++·算法
重庆兔巴哥2 小时前
如果Java环境变量配置不成功,应该怎么办?
java·开发语言
良木生香2 小时前
【C++初阶】:C++入门相关知识(3):引用 & inline内联函数 & nullptr相关概念
开发语言·c++