杰瑞科技汇

Java序列化如何实现?

什么是 Java 序列化?

Java 序列化(Serialization)是一种将 Java 对象的状态(即其成员变量的值)转换为字节流的过程,这个字节流可以持久化到磁盘(如保存为 .ser 文件),也可以通过网络传输到另一台 JVM(Java 虚拟机)上。

Java序列化如何实现?-图1
(图片来源网络,侵删)

与序列化相对应的是反序列化(Deserialization),它将字节流重新恢复成原来的 Java 对象。

核心目的: 实现对象的“持久化存储”和“网络传输”。


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

实现 Java 序列化非常简单,主要遵循以下两个步骤:

让类实现 Serializable 接口

这是最关键的一步,你想要序列化的类,必须实现 java.io.Serializable 接口。

Java序列化如何实现?-图2
(图片来源网络,侵删)
import java.io.Serializable;
// 必须实现 Serializable 接口
public class User implements Serializable {
    // ...
}

注意:

  • Serializable 是一个标记接口(Marker Interface),它内部没有任何方法,它就像一个“许可证”,告诉 JVM 这个类的对象可以被序列化。
  • 如果一个类没有实现 Serializable 接口,那么尝试序列化其实例时会抛出 NotSerializableException

使用 ObjectOutputStreamObjectInputStream 进行读写

  • 序列化(写入): 使用 ObjectOutputStream 将对象写入到输出流(如文件输出流)。
  • 反序列化(读取): 使用 ObjectInputStream 从输入流(如文件输入流)中读取对象并重建。

完整代码示例

我们创建一个 User 类,然后将其序列化到文件,再从文件中反序列化回来。

User.java (可序列化的类)

import java.io.Serializable;
public class User implements Serializable {
    // 序列化版本号,强烈建议添加
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    // 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 != null ? "******" : "null") + '\'' + // 出于安全考虑,打印时隐藏密码
                '}';
    }
}

SerializationDemo.java (演示序列化和反序列化)

Java序列化如何实现?-图3
(图片来源网络,侵删)
import java.io.*;
public class SerializationDemo {
    public static void main(String[] args) {
        // --- 1. 序列化:将对象写入文件 ---
        User user = new User("张三", 30, "123456");
        String fileName = "user.ser";
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName))) {
            // writeObject() 方法负责将对象序列化
            oos.writeObject(user);
            System.out.println("对象序列化成功,已保存到 " + fileName);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // --- 2. 反序列化:从文件中读取对象 ---
        User deserializedUser = null;
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName))) {
            // readObject() 方法负责从字节流中反序列化对象
            deserializedUser = (User) ois.readObject();
            System.out.println("对象反序列化成功:");
            System.out.println(deserializedUser);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

对象序列化成功,已保存到 user.ser
对象反序列化成功:
User{name='张三', age=30, password='null'}

结果分析:

  • nameage 被成功序列化和反序列化。
  • password 字段为 null,因为它被 transient 关键字修饰,ObjectOutputStream 会自动跳过它。

深入理解:serialVersionUIDtransient

serialVersionUID (序列化版本号)

serialVersionUID 是一个 private static final long 类型的常量,它的作用是版本控制

  • 为什么需要它? 在反序列化时,JVM 会会检查字节流中的 serialVersionUID 与当前类的 serialVersionUID 是否一致。

    • 一致: 如果类的定义没有发生破坏性变化(如删除了字段),反序列化成功。
    • 不一致: JVM 会认为字节流对应的类版本和当前类版本不兼容,抛出 InvalidClassException
  • 什么时候会不一致? 当你修改了类的结构,

    • 增加/删除了一个字段
    • 修改了字段的类型
    • 修改了类的继承关系
    • 等等...
  • 最佳实践: 始终为可序列化的类显式声明一个 serialVersionUID 这样可以让你在类结构发生变化时,主动控制版本兼容性,如果不显式声明,JVM 会根据类的结构自动生成一个,但这种自动生成的方式非常脆弱,任何一个微小的改动(比如修改注释)都可能导致其值改变,从而破坏反序列化。

transient 关键字

transient 关键字用于标记那些不希望被序列化的成员变量。

  • 常见使用场景:

    1. 敏感信息: 如密码、密钥等,这些信息不应该被持久化或通过网络传输。
    2. 非可序列化对象: 如果一个类的某个成员变量本身是不可序列化的(如 java.io.File 对象、数据库连接 Connection 等),你必须将其标记为 transient,否则序列化时会报错。
    3. 计算得出或可恢复的数据: 某些字段可以通过其他字段计算得出,或者可以在反序列化后重新初始化,那么就没有必要序列化它们。
  • transient 字段的默认值:

    • 对于对象类型:null
    • 对于基本数据类型:0 (int), false (boolean) 等

Java 序列化的优缺点

优点:

  1. 简单易用: API 设计非常直观,只需实现接口和调用方法即可。
  2. JVM 内置支持: 无需引入第三方库,是 Java 标准库的一部分。
  3. 支持对象图: 可以自动处理一个对象内部引用的其他对象(整个对象图)。

缺点:

  1. 安全性差:
    • 反序列化过程可以执行任意代码,存在严重的安全漏洞(如 Apache Commons Collections 反序列化漏洞)。
    • 敏感数据如果忘记用 transient 修饰,会直接暴露。
  2. 性能较低: 序列化和反序列化的过程比较耗时,且会产生较大的二进制数据,不适合高性能场景。
  3. 跨语言性差: Java 序列化是 Java 语言特有的,其他语言(如 Python, C++, Go)无法直接解析 .ser 文件,这使得它不适用于微服务架构中不同语言服务间的通信。
  4. 版本兼容性脆弱: 如前所述,类的任何结构变化都可能导致反序列化失败,维护成本高。

现代替代方案

由于 Java 原生序列化的诸多缺点,在现代应用开发中,尤其是在分布式系统和微服务架构中,我们更倾向于使用更通用、更高效、更安全的跨语言序列化方案。

JSON (JavaScript Object Notation)

  • 特点: 轻量级、文本格式、易于阅读和编写、跨语言支持最好。
  • 常用库:
    • Jackson: 性能最好,功能最强大,是 Spring Boot 的默认 JSON 库。
    • Gson: Google 出品,API 简单易用。
    • Fastjson: 阿里巴巴出品,性能极佳,但历史上曾曝出过安全漏洞,需谨慎使用新版本。
  • 适用场景: Web API (RESTful) 响应、配置文件、日志等。

XML (eXtensible Markup Language)

  • 特点: 标签化、可扩展性好、格式严谨,但冗余信息较多,比 JSON 更重。
  • 常用库:
    • JAXB: Java 标准库的一部分,支持注解,可以将 Java 对象直接与 XML 绑定。
    • DOM / SAX: 底层的 XML 解析 API
分享:
扫描分享到社交APP
上一篇
下一篇