什么是 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. 对机制与细节的掌握:不仅仅是知道 implements Serializable,更想知道候选人是否理解其背后的机制,如 序列化版本 UID (serialVersionUID) 的作用、transient 关键字的使用,以及序列化过程的底层细节。
  3. 对应用场景与替代方案的认知:候选人是否能在实际开发中(如 RPC 调用、对象缓存、持久化存储)正确应用序列化,并了解其性能、安全方面的局限性以及主流的替代方案(如 JSON、Protocol Buffers)。
  4. 对安全与最佳实践的了解:是否意识到 Java 原生序列化的潜在安全风险(如反序列化漏洞),以及在工程实践中应遵循的规范。

核心答案

Java 序列化 是指将内存中的 Java 对象(Object)转换成一个可存储或可传输的字节序列(byte stream)的过程。 反序列化 则是其逆过程,将字节序列恢复为内存中的 Java 对象。

其核心目的是实现对象的持久化存储(如存入文件或数据库)和网络传输(如远程方法调用 RPC 中的参数传递)。一个类通过实现 java.io.Serializable 接口来声明其对象是可序列化的。

深度解析

原理/机制

序列化的本质是按照特定协议,将对象的状态信息(即成员变量的值)转换为字节流。它并不保存方法、静态变量等信息。

  • Serializable 接口:这是一个标记接口(Marker Interface),没有任何方法,仅用于标识该类允许被序列化。
  • serialVersionUID:这是一个至关重要的私有静态常量。它相当于类的“指纹”或版本号。在反序列化时,JVM 会比较字节流中的 serialVersionUID 与本地类的 serialVersionUID。如果不一致,将抛出 InvalidClassException显式声明一个固定的 serialVersionUID 是强烈推荐的最佳实践,它可以避免因类结构发生微小变更(如增加一个无关紧要的方法)而导致的反序列化失败。
  • transient 关键字:用于修饰成员变量,表示该变量在序列化时应被忽略。常用于保存敏感信息(如密码)或不支持序列化的对象引用。

代码示例

// 1. 实现 Serializable 接口
public class User implements Serializable {
    // 2. 强烈建议显式声明 serialVersionUID
    private static final long serialVersionUID = 1L;
    
    private String name;
    private int age;
    // 3. 使用 transient 关键字阻止序列化
    private transient String password;
    
    // 构造方法、getter、setter 省略...
}

// 序列化与反序列化过程
public class SerializationDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        User user = new User("Alice", 30, "secret");
        
        // --- 序列化到文件 ---
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("user.dat"))) {
            oos.writeObject(user); // 关键步骤
        }
        
        // --- 从文件反序列化 ---
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("user.dat"))) {
            User deserializedUser = (User) ois.readObject(); // 关键步骤
            System.out.println(deserializedUser.getName()); // 输出: Alice
            System.out.println(deserializedUser.getPassword()); // 输出: null (因为被 transient 修饰)
        }
    }
}

对比分析与最佳实践

  • Java 原生序列化 vs. 其他序列化方案

    • Java 原生序列化:深度集成于 JVM,使用方便。但缺点显著:生成的字节流体积大、序列化/反序列化性能较差、存在安全漏洞风险(通过构造恶意字节流可执行任意代码)。
    • JSON/XML:文本格式,人类可读,跨语言支持好,常用于 Web API。但文本解析效率低于二进制,且无法直接序列化复杂的对象图(如循环引用)。
    • Protocol Buffers / Apache Thrift / Apache Avro:高效的二进制序列化方案,跨语言、高性能、体积小、有清晰的模式定义(Schema),是现代 RPC 框架(如 gRPC)和大数据系统的首选。
  • 最佳实践与常见误区

    1. 显式声明 serialVersionUID:这是防止不兼容反序列化的第一道防线。
    2. 谨慎序列化:不要序列化整个大对象图,只序列化必要的数据。
    3. 处理好 transient 变量:反序列化后,transient 变量会被设为默认值(如 null, 0)。如果其值依赖于其他状态,可能需要在 readObjectwriteObject 方法中自定义逻辑。
    4. 注意安全永远不要反序列化来自不可信来源的数据。可以考虑使用白名单机制(ObjectInputFilter,JDK 9+ 引入)来限制反序列化的类。
    5. 考虑替代方案:在新项目中,除非有强兼容性需求,否则应优先考虑使用 JSON(如 Jackson/Gson)或高效的二进制协议(如 Protobuf)。

总结

Java 序列化是将对象转为字节流以便存储或传输的机制,通过实现 Serializable 接口来启用,核心要点在于管理好 serialVersionUID 和合理使用 transient;但由于其性能和安全缺陷,在实际高性能、跨语言系统中,更推荐使用 JSON 或 Protobuf 等现代序列化方案。