杰瑞科技汇

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

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

它们是将对象转换为字节流将字节流恢复为对象的过程。

Java对象如何序列化与反序列化?-图1
(图片来源网络,侵删)
  • 序列化:将 Java 对象(及其状态)转换成一种字节序列(或称为二进制流)的过程,这个字节序列可以保存在文件、数据库中,或者通过网络传输。
  • 反序列化:将这个字节序列重新恢复成原来的 Java 对象的过程。

为什么需要它? Java 的程序运行在内存中,而内存是易失的,当程序关闭或服务器重启时,内存中的所有对象都会丢失,序列化提供了一种机制,可以将对象的状态持久化,以便在之后可以重新创建它,它也是 Java RMI(远程方法调用)、Java 消息队列等技术的基石。


如何实现序列化和反序列化?

Java 提供了一套内置的 API 来实现这个过程,核心是 java.io.Serializable 接口和 ObjectOutputStream / ObjectInputStream 类。

核心概念:Serializable 接口

一个类要想被序列化,必须实现 java.io.Serializable 接口,这个接口是一个标记接口,它没有任何方法或字段,只是起到一个标识作用,告诉 JVM:“这个类的对象可以被序列化”。

import java.io.Serializable;
// 实现了 Serializable 接口,这个类的对象就可以被序列化了
public class User implements Serializable {
    // ... 类定义 ...
}

核心类:ObjectOutputStreamObjectInputStream

  • ObjectOutputStream:负责将对象序列化成字节流。
    • writeObject(Object obj):将指定的对象写入输出流。
  • ObjectInputStream:负责从字节流反序列化成对象。
    • readObject():从输入流中读取一个对象。

完整示例代码

我们通过一个完整的例子来演示整个过程。

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

第1步:创建一个可序列化的 User

import java.io.Serializable;
// 必须实现 Serializable 接口
public class User implements Serializable {
    // 序列化版本号,强烈推荐添加,后面会解释为什么
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    private transient String password; // 使用 transient 关键字
    // 构造方法
    public User(String name, int age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }
    // Getters and Setters
    public String getName() { return name; }
    public int getAge() { return age; }
    public String getPassword() { return password; }
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", password='" + (password != null ? "******" : "null") + '\'' +
                '}';
    }
}

注意

  • serialVersionUID:这是一个唯一的版本标识符,当类结构(如增删改字段)发生变化时,JVM 会根据类的结构计算出一个新的 serialVersionUID,如果反序列化时,文件的 serialVersionUID 与当前类的 serialVersionUID 不匹配,就会抛出 InvalidClassException,手动定义它可以避免因类结构微小变动(比如增加一个无关紧要的字段)导致的序列化/反序列化失败。
  • transient 关键字:用 transient 修饰的字段不会被序列化,对于一些敏感信息(如密码)或不需要持久化的状态(如 Thread 对象),我们会使用 transient,反序列化后,这些字段的值会被设为对应类型的默认值(如 null, 0, false)。

第2步:编写序列化和反序列化的代码

import java.io.*;
public class SerializationDemo {
    public static void main(String[] args) {
        // 1. 创建一个 User 对象
        User user = new User("Alice", 30, "123456");
        // --- 序列化过程:将对象写入文件 ---
        String fileName = "user.ser";
        serializeObject(user, fileName);
        // --- 反序列化过程:从文件中读取对象 ---
        User deserializedUser = deserializeObject(fileName);
        // 3. 验证结果
        System.out.println("原始对象: " + user);
        System.out.println("反序列化后的对象: " + deserializedUser);
        System.out.println("两个对象是否相等: " + user.equals(deserializedUser)); // 注意:这里比较的是地址,实际可能需要重写 equals
    }
    /**
     * 序列化对象到文件
     * @param obj 要序列化的对象
     * @param fileName 目标文件名
     */
    private static void serializeObject(Object obj, String fileName) {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName))) {
            oos.writeObject(obj);
            System.out.println("对象已成功序列化到文件: " + fileName);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 从文件反序列化对象
     * @param fileName 源文件名
     * @return 反序列化后的对象
     */
    private static User deserializeObject(String fileName) {
        User user = null;
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName))) {
            user = (User) ois.readObject();
            System.out.println("对象已成功从文件反序列化: " + fileName);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return user;
    }
}

运行结果

对象已成功序列化到文件: user.ser
对象已成功从文件反序列化: user.ser
原始对象: User{name='Alice', age=30, password='******'}
反序列化后的对象: User{name='Alice', age=30, password='null'}
两个对象是否相等: false

从结果可以看出,password 字段因为被 transient 修饰,在反序列化后变成了 null


序列化的高级特性

1 serialVersionUID 的作用详解

serialVersionUID 是序列化机制中的“身份证号”。

  • 显式声明:如示例中所示,private static final long serialVersionUID = 1L;
  • 隐式生成:如果没有显式声明,JVM 会在编译时根据类的结构(字段、方法、接口等)自动生成一个,这个生成过程依赖于编译器的实现,并且在不同版本(如不同 JDK)或不同编译器下可能不同。

为什么必须要有? 假设你序列化了一个 User 对象到文件,然后修改了 User 类,比如增加了一个 String email 字段,现在你尝试用修改后的类去反序列化那个文件,会发生什么?

  • 没有 serialVersionUID:JVM 会发现文件中的类结构和当前类的结构不一致(文件里没有 email 字段,当前类有),于是抛出 InvalidClassException
  • serialVersionUID:只要类的核心逻辑没有发生根本性变化(比如删除了父类或实现了新的接口),即使增加了新字段,只要 serialVersionUID 相同,JVM 就会尝试兼容,它会将文件中已有的字段正确赋值,而新增的字段会被设为默认值,这提供了极大的灵活性。

2 writeObjectreadObject 方法

如果你想在序列化或反序列化时执行一些自定义逻辑,可以在类中定义 privatewriteObjectreadObject 方法。

import java.io.*;
public class CustomSerialization implements Serializable {
    private String data;
    private transient String secret;
    public CustomSerialization(String data, String secret) {
        this.data = data;
        this.secret = secret;
    }
    /**
     * 自定义序列化逻辑
     * @param oos ObjectOutputStream
     * @throws IOException
     */
    private void writeObject(ObjectOutputStream oos) throws IOException {
        // 默认序列化逻辑
        oos.defaultWriteObject();
        // 自定义逻辑:将 transient 字段手动写入
        oos.writeObject("SECRET_PREFIX_" + secret);
    }
    /**
     * 自定义反序列化逻辑
     * @param ois ObjectInputStream
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        // 默认反序列化逻辑
        ois.defaultReadObject();
        // 自定义逻辑:手动读取 transient 字段
        String prefixedSecret = (String) ois.readObject();
        this.secret = prefixedSecret.replace("SECRET_PREFIX_", "");
    }
    @Override
    public String toString() {
        return "CustomSerialization{" +
                "data='" + data + '\'' +
                ", secret='" + secret + '\'' +
                '}';
    }
}

在这个例子中,即使 secrettransient 的,我们也通过自定义方法将其序列化了,并且在反序列化时恢复了它。


序列化的替代方案

虽然 Serializable 很方便,但它也有一些缺点,

  • 性能开销:反射机制导致性能较低。
  • 安全性问题:反序列化可能被恶意利用,导致远程代码执行漏洞(详见下文)。
  • 非侵入性:需要修改类源码来实现接口,不够灵活。

在现代 Java 开发中,有几种更优的替代方案:

1 JSON / XML 格式

这是目前最主流的方式,将对象转换为文本格式的 JSON 或 XML。

  • 优点
    • 可读性强:人类可以读懂,便于调试。
    • 跨语言:几乎所有编程语言都支持 JSON 和 XML。
    • 安全性高:没有反序列化漏洞的风险。
    • 灵活性高:不需要实现任何接口。
  • 常用库
    • JSON: Jackson, Gson, Fastjson (阿里巴巴)。
    • XML: JAXB (Java 内置), XStream。

示例 (使用 Jackson):

// 添加 Jackson 依赖
// implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0'
import com.fasterxml.jackson.databind.ObjectMapper;
public class JsonSerializationDemo {
    public static void main(String[] args) throws Exception {
        User user = new User("Bob", 25, "654321");
        // 1. 序列化成 JSON 字符串
        ObjectMapper mapper = new ObjectMapper();
        String jsonString = mapper.writeValueAsString(user);
        System.out.println("JSON 序列化结果: " + jsonString);
        // 2. 反序列化成对象
        User fromJson = mapper.readValue(jsonString, User.class);
        System.out.println("JSON 反序列化结果: " + fromJson);
    }
}

2 Java 外部化接口 (Externalizable)

ExternalizableSerializable 的子接口,提供了更细粒度的控制。

  • 区别
    • Serializable:采用默认机制,自动序列化所有非 transient 字段。
    • Externalizable:需要手动实现 writeExternal()readExternal() 方法,完全由你来控制哪些字段被序列化以及如何序列化。
  • 优点:性能更高,控制力更强。
  • 缺点:代码更繁琐,需要自己处理所有字段。

安全风险:反序列化漏洞

这是一个非常重要的安全问题,当反序列化不可信的数据源(如网络请求、用户上传的文件)时,攻击者可以构造恶意的字节流,使其在反序列化时执行任意代码。

攻击原理: 攻击者可以创建一个包含恶意代码的类(MaliciousClass),并重写其 readObject() 方法,然后将这个类的对象序列化成字节流发送给你的程序,当你的程序调用 ObjectInputStream.readObject() 时,JVM 会调用 MaliciousClassreadObject() 方法,从而执行其中的恶意代码。

防御措施

  1. 不要反序列化不可信的数据:这是最根本的原则。
  2. 使用白名单:只允许反序列化已知的、安全的类。
  3. 使用安全的替代方案:优先使用 JSON 等文本格式。
  4. 使用安全库:如 SerialKiller 可以在反序列化前检查类的黑/白名单。
  5. 保持库更新:许多序列化库(如 Jackson)会及时修复已知的安全漏洞。

特性 Serializable Externalizable JSON / XML
实现方式 实现接口,无需编写代码 实现接口,需手动编写 writeExternal/ReadExternal 使用第三方库,无需修改类
控制力 低(自动序列化所有非 transient 字段) 高(完全手动控制) 高(通过注解或配置控制)
性能 较低(使用反射) 较高(不使用反射) 较高(解析文本)
可读性 二进制,不可读 二进制,不可读 文本,可读
跨语言
安全性 较低(有反序列化漏洞风险) 较低(同样有风险) (无漏洞风险)
使用场景 简单的、内部对象的持久化(如 RMI) 对性能有极高要求的场景 Web API、配置文件、跨系统通信等现代场景

建议

  • 对于内部系统、简单、对性能要求不高的对象,如果必须使用二进制序列化,Serializable 是一个快速的选择。
  • 对于Web 服务、API、跨语言通信、任何涉及用户输入或网络传输的场景强烈推荐使用 JSON 等文本格式,它更安全、更灵活、更通用。
分享:
扫描分享到社交APP
上一篇
下一篇