什么是 Java 序列化与反序列化?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

  1. 概念理解:面试官不仅仅是想知道 "对象变字节、字节变对象" 这个定义,更是想考察你是否理解序列化的本质——将内存中的对象状态转换为可存储/可传输的格式。

  2. 实践应用:序列化在实际开发中无处不在(RPC 调用、缓存存储、消息队列、深拷贝等),考察你是否知道何时需要序列化,以及如何选择合适的序列化方案。

  3. 安全意识:Java 原生序列化存在安全漏洞(反序列化攻击),考察你是否了解这些风险以及如何在生产环境中规避。

核心答案

序列化是将 Java 对象转换为字节序列的过程,便于存储到文件、数据库或进行网络传输;反序列化是将字节序列恢复为 Java 对象的过程。

操作输入输出典型场景
序列化Java 对象字节序列存储到磁盘、网络传输、分布式调用
反序列化字节序列Java 对象从磁盘读取、接收网络数据、RPC 响应

一句话理解:序列化让对象 "离开 JVM"(持久化/传输),反序列化让对象 "回到 JVM"(恢复使用)。

深度解析

一、序列化与反序列化流程

上图展示了序列化与反序列化的完整流程。关键要点:

  • 序列化阶段:通过 ObjectOutputStream.writeObject() 将内存中的 Java 对象转换为字节序列,可以写入文件、存入数据库、或通过网络发送。

  • 传输/存储阶段:字节序列以二进制形式存在,不依赖于具体的 JVM,可以跨机器、跨时间传输。

  • 反序列化阶段:通过 ObjectInputStream.readObject() 将字节序列重新构建为内存中的 Java 对象,恢复对象的状态。

二、Java 原生序列化实现

基本用法

import java.io.*;

// 实现 Serializable 接口(标记接口,无方法)
class User implements Serializable {
    private static final long serialVersionUID = 1L;  // 版本号

    private String name;
    private Integer age;
    private transient String password;  // transient 关键字:不参与序列化

    // 构造方法、getter、setter...
    public User(String name, Integer age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{name='" + name + "', age=" + age + ", password='" + password + "'}";
    }
}

public class SerializationDemo {
    public static void main(String[] args) throws Exception {
        User user = new User("张三", 25, "123456");

        // ===== 序列化:对象 → 字节 =====
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(user);
        byte[] bytes = bos.toByteArray();
        System.out.println("序列化完成,字节数: " + bytes.length);

        // ===== 反序列化:字节 → 对象 =====
        ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
        ObjectInputStream ois = new ObjectInputStream(bis);
        User deserializedUser = (User) ois.readObject();
        System.out.println("反序列化结果: " + deserializedUser);
        // 输出: User{name='张三', age=25, password='null'}
        // 注意:password 被 transient 修饰,反序列化后为 null
    }
}

关键点说明

关键字/接口作用说明
Serializable标记接口表示类可序列化,无任何方法
serialVersionUID版本标识保证序列化/反序列化版本一致性
transient排除字段被修饰的字段不参与序列化
static类变量不参与序列化(属于类,不属于对象)

三、serialVersionUID 的重要性

// 场景:序列化后,类结构发生了变化
class User implements Serializable {
    private static final long serialVersionUID = 1L;  // 必须显式定义!

    private String name;
    // private String email;  // 新增字段后,如果不定义 serialVersionUID 会报错!
}

为什么必须显式定义 serialVersionUID

  • 如果不定义,JVM 会根据类结构自动生成一个版本号
  • 类结构变化(新增/删除字段)后,自动生成的版本号会改变
  • 反序列化时版本号不匹配,抛出 InvalidClassException

最佳实践:所有可序列化的类都必须显式定义 serialVersionUID,使用 IDE 自动生成即可。

四、常见序列化方案对比

上图对比了常见序列化方案的特点。选择建议:

  • Java 原生序列化:仅用于简单场景或深拷贝,生产环境不推荐(性能差、体积大、有安全风险)

  • JSON(Jackson/Gson):前后端交互首选,可读性好,调试方便,但性能和体积不如二进制方案

  • Protobuf:高性能 RPC 场景首选,需要定义 .proto 文件,跨语言支持好

  • Hessian/Kryo:Java 内部 RPC 场景,无需定义 schema,性能优秀

五、序列化的安全风险

// 危险!不要反序列化不可信的数据
ObjectInputStream ois = new ObjectInputStream(untrustedInput);
Object obj = ois.readObject();  // 可能触发恶意代码执行!

Java 原生序列化的安全风险

  • 反序列化攻击:攻击者构造恶意的字节序列,反序列化时会执行任意代码(RCE)
  • 典型案例:Apache Commons Collections 反序列化漏洞(2015 年)
  • 解决方案
    • 避免使用 Java 原生序列化处理不可信数据
    • 使用 JSON 等安全的序列化方案
    • 使用白名单机制限制可反序列化的类

面试高频追问

  1. SerializableExternalizable 有什么区别?

    接口序列化控制性能复杂度
    Serializable自动序列化所有非 transient 字段较低简单
    Externalizable完全自定义 writeExternal()readExternal()较高复杂

    生产环境推荐使用 Externalizable 或第三方序列化框架,性能更好。

  2. 为什么 static 字段不参与序列化?

    序列化的是对象的状态,而 static 字段属于,不属于对象。所有对象共享同一个 static 字段,序列化它没有意义。

  3. 父类没有实现 Serializable,子类可以序列化吗?

    可以,但父类的字段不会被序列化。反序列化时,父类会调用无参构造方法初始化字段。

常见面试变体

  • 变体一:"transient 关键字的作用是什么?"
  • 变体二:"为什么阿里开发手册不建议使用 Java 原生序列化?"
  • 变体三:"RPC 框架中常用的序列化方案有哪些?"

记忆口诀

对象变字节叫序列,字节变对象叫反序列; Serializable 标记接口,serialVersionUID 版本要定好; transient 不参与,原生序列化有风险要少用。

总结

序列化将 Java 对象转换为字节序列,便于存储和传输;反序列化将字节恢复为对象。Java 原生序列化通过 Serializable 接口实现,但存在性能差、体积大、安全风险等问题,生产环境推荐使用 JSON、Protobuf 等更优方案。务必显式定义 serialVersionUID,敏感字段使用 transient 排除。