杰瑞科技汇

Java对象如何序列化与反序列化?

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

这是一个将对象转换为字节流以及将字节流恢复为对象的过程。

Java对象如何序列化与反序列化?-图1
(图片来源网络,侵删)
  • 序列化:将 Java 对象转换为字节流或其他数据传输格式的过程,这个过程可以让你将对象的状态保存到文件、数据库,或者通过网络传输到另一台机器。
  • 反序列化:将字节流或其他数据传输格式恢复为原始 Java 对象的过程,这个过程让你能够从文件、数据库或网络中读取数据,并重新构建出对象。

一个生动的比喻: 想象一下你要把一个复杂的乐高模型(Java 对象)通过邮寄(网络传输)送给朋友。

  1. 序列化:你把乐高模型拆解成一个个零件(字节流),并打包好(写入文件/网络包)。
  2. 运输:包裹(字节流)被邮寄给朋友。
  3. 反序列化:朋友收到包裹后,根据说明书(协议)把零件重新组装起来,得到了一个和原来一模一样的乐高模型(Java 对象)。

为什么需要序列化和反序列化?

序列化机制在 Java 中有着非常广泛和重要的用途:

  1. 网络传输:当你在进行远程方法调用(如 RMI)或者通过网络发送对象时,对象需要被序列化成字节流才能在网络上传输。
  2. 持久化存储:你想把一个对象的状态保存到硬盘上,以便程序下次启动时能够恢复它,保存游戏进度、用户会话信息等。
  3. 深度克隆:通过序列化和反序列化,可以创建一个对象的深拷贝(Deep Copy),这种方法简单且能保证克隆的完整性。
  4. 跨 JVM 通信:在不同的 Java 虚拟机之间传递对象时,序列化是标准的通信方式。

如何实现序列化和反序列化?(核心步骤)

要让一个 Java 对象可以被序列化,最关键的一步是:

让该类实现 java.io.Serializable 接口。

Java对象如何序列化与反序列化?-图2
(图片来源网络,侵删)

Serializable 是一个标记接口(Marker Interface),它里面没有任何方法,它的作用仅仅是告诉 JVM:“这个类的对象可以被序列化”。

示例代码

我们创建一个 Student 类,并让它实现 Serializable 接口。

import java.io.Serializable;
// 1. 实现 Serializable 接口
public class Student implements Serializable {
    // 2. 建议添加一个序列化版本号 UID (SerialVersionUID)
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    private transient String password; // transient 关键字见下文
    public Student(String name, int age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", password='" + password + '\'' +
                '}';
    }
}

代码解释:

  • implements Serializable:这是必须的。
  • serialVersionUID:这是一个非常重要的字段,它相当于对象的“身份证号”,在进行反序列化时,JVM 会比较字节流中的 serialVersionUID 和本地类的 serialVersionUID,如果两者不一致,会抛出 InvalidClassException强烈建议在所有可序列化的类中显式定义 serialVersionUID,这样可以避免因类结构(如修改字段、增删方法)的微小变动导致反序列化失败。
  • transient 关键字:用 transient 修饰的字段不会被序列化,这在处理敏感信息(如密码)或不需要持久化的数据时非常有用,上面的 password 字段就不会被保存到文件中。

序列化和反序列化的 API

Java 提供了两个核心类来处理这个过程:

Java对象如何序列化与反序列化?-图3
(图片来源网络,侵删)
  • ObjectOutputStream:用于将对象写入到输出流(如文件、网络)。
  • ObjectInputStream:用于从输入流(如文件、网络)中读取对象并重建。

完整示例

我们将一个 Student 对象序列化到 student.ser 文件中,然后再从该文件中反序列化出来。

import java.io.*;
public class SerializationDemo {
    public static void main(String[] args) {
        // 创建一个 Student 对象
        Student originalStudent = new Student("Alice", 20, "123456");
        // --- 1. 序列化过程 ---
        // try-with-resources 语句,确保流被自动关闭
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.ser"))) {
            oos.writeObject(originalStudent);
            System.out.println("对象已成功序列化到 student.ser 文件。");
        } catch (IOException e) {
            e.printStackTrace();
        }
        // --- 2. 反序列化过程 ---
        Student deserializedStudent = null;
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.ser"))) {
            // 从文件中读取对象
            deserializedStudent = (Student) ois.readObject();
            System.out.println("对象已成功从 student.ser 文件反序列化。");
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        // --- 3. 验证结果 ---
        if (deserializedStudent != null) {
            System.out.println("原始对象: " + originalStudent);
            System.out.println("反序列化对象: " + deserializedStudent);
            // 注意:password 字段是 transient,所以为 null
            System.out.println("反序列化后的密码: " + deserializedStudent.getPassword());
        }
    }
}

输出结果:

对象已成功序列化到 student.ser 文件。
对象已成功从 student.ser 文件反序列化。
原始对象: Student{name='Alice', age=20, password='123456'}
反序列化对象: Student{name='Alice', age=20, password='null'}
反序列化后的密码: null

可以看到,反序列化得到的对象除了 transient 字段 passwordnull 外,其他信息都完美恢复了。


高级话题和注意事项

1 serialVersionUID 的作用详解

  • 显式声明:如上所述,推荐显式声明。
  • 隐式生成:如果你没有显式声明,JVM 会在编译时根据类的结构(类名、接口、字段、方法等)自动计算一个 serialVersionUID这个计算过程对编译器的实现细节非常敏感,一旦你对类做了任何微小的改动(比如增加一个字段、修改一个方法的访问修饰符),JVM 重新计算出的 serialVersionUID 就会不同,导致反序列化时抛出 InvalidClassException

2 Externalizable 接口

ExternalizableSerializable 的子接口,它提供了更高级的控制。

  • Serializable:JVM 自动完成序列化过程,你只需要声明类可序列化即可,这种方式简单,但性能开销较大,因为它会递归地序列化对象图中的所有对象。
  • Externalizable:你需要手动实现 writeExternal(ObjectOutput out)readExternal(ObjectInput in) 方法来控制序列化和反序列化的具体过程,这提供了更高的性能和灵活性,但你需要自己处理所有字段的读写,并且要负责处理父类的序列化。

使用场景:当你对性能要求极高,或者需要以特定格式(如 JSON、XML)序列化对象时,可以考虑 Externalizable

3 继承与序列化

  • 如果一个类实现了 Serializable,那么它的所有子类也都默认是可序列化的。
  • 父类如果没有实现 Serializable,但子类实现了,那么在序列化子类对象时,父类的字段需要通过反射机制来调用无参构造函数进行初始化,如果父类没有无参构造函数,就会抛出 InvalidClassException

4 安全性考虑

序列化/反序列化过程可能带来安全风险,特别是当从不受信任的来源(如网络)接收字节流时。

  • 反序列化漏洞:攻击者可以精心构造一个恶意的字节流,其中包含一个指向特定类(如 java.lang.Runnable 的恶意实现)的引用,当目标程序反序列化这个字节流时,会尝试加载并执行这个恶意类,可能导致远程代码执行。
  • 防御措施
    1. 不信任输入:永远不要反序列化来自不可信来源的数据。
    2. 使用白名单:在反序列化之前,检查要加载的类是否在你的安全白名单中。
    3. 使用替代方案:对于需要传输的数据,优先使用 JSON、XML 等文本格式,而不是二进制的 Java 序列化,这些格式更安全、更通用。

现代替代方案

传统的 Java 序列化虽然强大,但也存在一些缺点(如性能差、不安全、跨语言性差),在现代应用中,我们更倾向于使用以下替代方案:

方案 格式 优点 缺点 适用场景
JSON 文本 轻量级、可读性好、跨语言、有成熟的库(如 Jackson, Gson) 需要手动定义转换逻辑或使用注解 Web API (RESTful)、配置文件、日志
XML 文本 可读性好、支持命名空间和复杂结构 冗长、解析速度慢 企业级应用配置、Web 服务
Protocol Buffers / gRPC 二进制 高性能、高效率、强类型、跨语言 需要先定义 .proto 文件并编译生成代码 微服务、高性能 RPC 通信
Kryo 二进制 非常快,比 Java 原生序列化性能高得多 不是标准库,依赖第三方 游戏、大数据处理、高性能计算
  • 如果你在 JVM 之间传递对象,且对性能和安全性要求不高,传统的 Serializable 仍然可用。
  • 如果你在构建 Web 服务或需要跨语言交互,JSON 是事实上的标准。
  • 如果你追求极致的性能,Protocol BuffersKryo 是更好的选择。

特性 描述
核心接口 java.io.Serializable (标记接口)
核心类 ObjectOutputStream (序列化), ObjectInputStream (反序列化)
关键步骤 类实现 Serializable
使用 ObjectOutputStream.write()
使用 ObjectInputStream.read()
重要字段 serialVersionUID:用于版本控制,防止反序列化失败。
关键字 transient:标记不被序列化的字段。
高级接口 Externalizable:提供手动控制序列化过程的能力,性能更高。
安全风险 反序列化可能被利用进行远程代码攻击。
现代替代 JSON, Protocol Buffers, Kryo 等,它们更安全、高效和通用。
分享:
扫描分享到社交APP
上一篇
下一篇