基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

Java中的序列化和反序列化

知识点图片

我们来详细、系统地讲解一下 Java 中的序列化(Serialization)和反序列化(Deserialization)。

1. 什么是序列化和反序列化?

想象一下,你正在玩一个游戏,需要保存进度。游戏中的角色、装备、等级等信息都存储在内存中的 Java 对象里。当你关闭游戏时,内存会被清空,这些对象就消失了。为了下次能从同一个地方继续,你需要把这些对象的状态“冻结”起来,存到一个文件里。这个“冻结”的过程就是 序列化

当你再次打开游戏,程序需要从文件中读取这些“冻结”的状态,并在内存中重新创建出原来的对象。这个“恢复”的过程就是 反序列化

核心定义:

  • 序列化 (Serialization): 将 Java 对象的状态信息转换为可以存储或传输的格式(通常是字节序列/字节流)的过程。这个字节流可以被写入文件、存入数据库,或者通过网络发送到另一台计算机。
  • 反序列化 (Deserialization): 从字节流中重新构建出原始 Java 对象的过程。它是序列化的逆过程。

2. 为什么要使用序列化?

序列化主要用于以下场景:

  1. 对象持久化 (Persistence): 将对象的状态保存到磁盘(如文件、数据库),以便在程序下次启动时可以恢复。这是最常见的用途,比如保存应用配置、用户会话、游戏存档等。
  2. 网络传输 (Communication): 在不同的 Java 应用程序之间(可能在不同的机器上)传递对象。例如,在远程方法调用(RMI, Remote Method Invocation)中,客户端将方法参数对象序列化后发送给服务器,服务器执行后将结果对象序列化后返回给客户端。
  3. 缓存 (Caching): 将一些计算成本高昂但不经常变化的对象序列化后存入缓存(如 Redis、Memcached),下次需要时直接反序列化获取,而无需重新计算。

3. 如何实现 Java 序列化?

在 Java 中实现序列化非常简单,主要涉及以下两个核心组件:

A. java.io.Serializable 接口

这是一个 标记接口(Marker Interface),它本身没有任何方法。一个类只需要实现这个接口,就等于向 JVM 声明:“我这个类的对象是可以被序列化的”。

java
import java.io.Serializable;

public class User implements Serializable {
    // ...
}

如果一个类实现了 Serializable 接口,那么它的所有非静态、非瞬时(transient)的字段都会被自动序列化。

B. ObjectOutputStreamObjectInputStream

这两个类是处理对象序列化和反序列化的关键 I/O 流。

  • ObjectOutputStream: 用于将对象写入输出流(如文件流),即执行 序列化 操作。核心方法是 writeObject(Object obj)
  • ObjectInputStream: 用于从输入流(如文件流)中读取字节并重建对象,即执行 反序列化 操作。核心方法是 readObject()

4. 代码示例

下面是一个完整的序列化和反序列化示例。

步骤 1: 创建一个可序列化的类

java
import java.io.Serializable;

// 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; // 密码等敏感信息不应该被序列化

    public User(String name, int age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", password='" + password + '\'' + // 注意看反序列化后 password 的值
                '}';
    }
}

步骤 2: 编写序列化和反序列化的主程序

java
import java.io.*;

public class SerializationDemo {
    public static void main(String[] args) {
        // 创建一个 User 对象
        User user = new User("Alice", 30, "123456");
        String filename = "user.ser"; // 序列化后的文件名,通常以 .ser 结尾

        // --- 1. 序列化过程 ---
        System.out.println("开始序列化...");
        try (FileOutputStream fileOut = new FileOutputStream(filename);
             ObjectOutputStream out = new ObjectOutputStream(fileOut)) {

            out.writeObject(user); // 将 user 对象写入文件
            System.out.println("对象已序列化到 " + filename);
            System.out.println("原始对象: " + user);

        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println("\n-----------------------------------\n");

        // --- 2. 反序列化过程 ---
        System.out.println("开始反序列化...");
        User deserializedUser = null;
        try (FileInputStream fileIn = new FileInputStream(filename);
             ObjectInputStream in = new ObjectInputStream(fileIn)) {

            deserializedUser = (User) in.readObject(); // 从文件读取并重建对象
            System.out.println("对象已从 " + filename + " 反序列化");
            System.out.println("反序列化后的对象: " + deserializedUser);

        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

plaintext
开始序列化...
对象已序列化到 user.ser
原始对象: User{name='Alice', age=30, password='123456'}

-----------------------------------

开始反序列化...
对象已从 user.ser 反序列化
反序列化后的对象: User{name='Alice', age=30, password='null'}

结果分析:

  • nameage 字段被成功保存和恢复。
  • password 字段因为被 transient 修饰,没有参与序列化,所以在反序列化后它的值为 null(对象的默认初始值)。

5. 重要的关键字和概念

a. transient 关键字

  • 作用: 阻止某个字段被序列化。
  • 用途:
    • 安全: 像密码、密钥这类敏感信息不应被序列化到文件中。
    • 无关数据: 某些字段是从其他字段计算得来的,或者代表一个临时的、不可序列化的资源(如数据库连接、线程池),这些字段不需要序列化。

b. serialVersionUID (序列版本号)

  • 作用: 它是类的版本控制标识符。在反序列化时,JVM 会检查文件中的 serialVersionUID 是否与当前 classpath 中该类的 serialVersionUID 一致。
  • 为什么重要:
    • 如果不一致: JVM 会抛出 InvalidClassException 异常,反序列化失败。这可以防止因类的版本不兼容导致的数据错乱问题。
    • 如果不显式定义: JVM 会根据类的结构(字段、方法等)自动生成一个。这样做很危险,因为只要你对类做了任何微小的改动(比如增加一个辅助方法),自动生成的 serialVersionUID 就可能改变,导致旧的序列化数据无法被新版本的类反序列化。
  • 最佳实践: 强烈建议 在所有可序列化的类中显式声明一个 private static final long serialVersionUID。通常初始值设为 1L 即可。如果你做了不兼容的类修改(比如删除了一个字段),则需要手动增加这个版本号。

c. 静态字段(static

静态字段属于类,不属于任何对象实例,因此 静态字段不会被序列化

d. 继承中的序列化

  • 如果一个父类实现了 Serializable,那么它的所有子类都自动是可序列化的。
  • 如果一个子类实现了 Serializable,但其父类没有,那么:
    • 子类的字段可以被序列化。
    • 父类的字段 不会 被序列化。
    • 在反序列化时,JVM 会调用父类的 无参构造函数 来初始化父类的部分。因此,这种情况下,父类 必须有一个无参构造函数,否则反序列化会失败。

6. 优缺点和替代方案

优点

  • 简单易用: Java 原生支持,只需实现一个接口,无需引入任何第三方库。
  • 功能强大: 能自动处理复杂的对象图(对象引用、循环引用等)。

缺点

  • 语言绑定: 序列化后的数据格式是 Java 特有的,只能被 Java 程序识别。不适合用于跨语言通信。
  • 版本兼容性问题: serialVersionUID 的管理需要格外小心,否则容易出现兼容性问题。
  • 安全性风险: 反序列化来自 不可信来源 的数据是一个严重的安全漏洞。攻击者可以构造恶意的字节流,在反序列化时执行任意代码(称为“反序列化攻击”)。
  • 性能和空间: Java 序列化产生的字节流通常比较臃肿,性能也不如一些现代的序列化框架。

现代替代方案

由于上述缺点,在很多现代应用(特别是分布式系统和微服务)中,开发者更倾向于使用以下替代方案:

  • JSON (Jackson, Gson): 人类可读,语言无关,是 Web API 和微服务通信的事实标准。
  • Protocol Buffers (Protobuf): Google 开发的高性能、紧凑的二进制序列化协议,跨语言支持极佳,广泛用于 RPC 框架(如 gRPC)。
  • Avro: Apache Hadoop 生态系统中的序列化系统,支持丰富的模式演进。
  • XML (JAXB): 曾经很流行,但因其冗长而逐渐被 JSON 取代。

总结

Java 原生序列化是一个强大且方便的功能,非常适合用于纯 Java 环境下的对象持久化和简单通信。但在构建需要跨语言、高性能或高安全性的现代分布式系统时,更推荐使用 JSON 或 Protocol Buffers 等更为通用的解决方案。理解其工作原理、特别是 transientserialVersionUID 的作用,对于编写健壮的 Java 应用至关重要。

00:00
00:00