杰瑞科技汇

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

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

这是将 Java 对象转换为可以存储或传输的格式,以及从该格式重新构建 Java 对象的过程。

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

为什么需要它? 想象一下场景:

  1. 持久化:你想把一个对象(比如一个用户信息 User 对象)保存到硬盘上,下次程序启动时再加载回来,直接保存对象是不行的,但序列化成字节流就可以。
  2. 网络传输:在分布式系统或微服务架构中,服务之间需要传递数据,对象不能直接在网络上传输,但可以被序列化成字节流(如 JSON、XML 或 Java 原生二进制格式)进行传输,接收方再反序列化成对象。

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

Java 提供了非常简单的 API 来实现这个过程,核心是 java.io.ObjectOutputStreamjava.io.ObjectInputStream

核心步骤:

  1. 让类实现 Serializable 接口 这是必须的第一步。Serializable 是一个标记接口,它里面没有任何方法,它的作用是告诉 JVM:“这个类的对象可以被序列化”,如果一个类没有实现这个接口,尝试序列化它的实例时会抛出 NotSerializableException

    import java.io.Serializable;
    public class User implements Serializable {
        // ...
    }
  2. 使用 ObjectOutputStream 进行序列化 这个类负责将对象写入到输出流中。

    Java对象序列化与反序列化如何实现?-图2
    (图片来源网络,侵删)
  3. 使用 ObjectInputStream 进行反序列化 这个类负责从输入流中读取字节序列并重建对象。


代码示例

下面是一个完整的、可运行的例子。

1 定义可序列化的类

import java.io.Serializable;
// 必须实现 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" : "******") + '\'' + // 出于安全考虑,不显示真实密码
                '}';
    }
}

关键点解释:

  • serialVersionUID:这是一个版本控制 ID,当类(User 类)的结构发生变化(如增删改字段)时,JVM 会根据类的结构计算出一个新的 serialVersionUID,如果反序列化时,文件的 serialVersionUID 与当前类的 serialVersionUID 不匹配,就会抛出 InvalidClassException,显式声明 serialVersionUID 可以确保在类结构发生非预期变化时(比如只改了一个注释),版本号依然可控,避免不必要的异常。
  • transient 关键字:用 transient 修饰的成员变量,在进行序列化时会被 JVM 忽略,不会被写入字节流,这在处理一些敏感信息(如密码)或不需要持久化的数据(如数据库连接)时非常有用。

2 执行序列化和反序列化操作

import java.io.*;
public class SerializationDemo {
    public static void main(String[] args) {
        // 1. 创建一个 User 对象
        User user = new User("Alice", 30, "123456");
        // 序列化:将对象写入文件
        serializeObject(user, "user.ser");
        // 反序列化:从文件中读取对象
        User deserializedUser = deserializeObject("user.ser");
        // 打印结果
        System.out.println("原始对象: " + user);
        System.out.println("反序列化后的对象: " + deserializedUser);
    }
    /**
     * 序列化方法
     * @param obj 要序列化的对象
     * @param filePath 文件路径
     */
    public static void serializeObject(Object obj, String filePath) {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath))) {
            oos.writeObject(obj);
            System.out.println("对象已成功序列化到 " + filePath);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 反序列化方法
     * @param filePath 文件路径
     * @return 反序列化后的对象
     */
    public static Object deserializeObject(String filePath) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath))) {
            System.out.println("正在从 " + filePath + " 反序列化对象...");
            return ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }
}

运行结果:

对象已成功序列化到 user.ser
正在从 user.ser 反序列化对象...
原始对象: User{name='Alice', age=30, password='******'}
反序列化后的对象: User{name='Alice', age=30, password='null'}

结果分析:

  • nameage 被正确序列化和反序列化。
  • password 字段因为被 transient 修饰,在序列化时被忽略,所以反序列化后值为 null

深入理解与注意事项

1 serialVersionUID 的重要性

假设 User 类被修改,增加了一个字段 email

public class User implements Serializable {
    private static final long serialVersionUID = 1L; // 保持不变
    private String name;
    private int age;
    private transient String password;
    private String email; // 新增字段
    // ...
}
  • serialVersionUID 相同 如果用旧版本的 User 对象(没有 email 字段)序列化后的文件,去反序列化新版本的 User 类(有 email 字段),JVM 会成功。email 字段的值会被赋予默认值(对于 String 是 null)。
  • serialVersionUID 不同 如果我们没有显式声明 serialVersionUID,JVM 会根据类的结构自动生成,增加 email 字段后,新的 serialVersionUID 会和旧文件中的不同,此时反序列化会直接抛出 InvalidClassException

始终显式声明 serialVersionUID 是一个好习惯。

.2 静态变量不会被序列化

序列化的是对象的状态(即实例变量),而不是类的状态(静态变量),静态变量属于类,不属于任何实例,因此它不会被序列化。

public class MyClass implements Serializable {
    public static int staticVar = 100;
    public int instanceVar = 200;
}

序列化一个 MyClass 对象,然后修改 staticVar 的值,再反序列化,你会发现,反序列化后的对象的 staticVar 值是修改后的值,而不是它被序列化时的值。

3 继承关系中的序列化

如果一个类实现了 Serializable,它的所有子类也都默认可序列化,如果父类没有实现 Serializable,子类实现了,那么在序列化子类对象时,父类的字段需要被特殊处理(通过反射调用 AccessibleObject.setAccessible 来强制访问)。

4 安全性考虑

  • 数据安全:永远不要序列化包含敏感信息(如密码、密钥)的字段,应该使用 transient 关键字。
  • 代码安全:反序列化不可信的数据源是极其危险的,攻击者可以构造恶意的字节流,在反序列化时执行任意代码(这被称为“反序列化漏洞”),只反序列化来自可信来源的数据。

序列化的替代方案

虽然 Java 原生的序列化很方便,但它也有一些缺点:

  • 性能较差:生成的字节流体积较大,且序列化/反序列化速度相对较慢。
  • 跨语言性差:它是 Java 特有的,其他语言(如 Python, C++)无法直接解析。
  • 安全问题:如上所述,存在反序列化漏洞的风险。

在现代应用中,尤其是在需要跨语言通信的场景下,通常会选择更通用、更高效的序列化框架:

方案 特点 适用场景
JSON 轻量级、文本格式、人可读、跨语言支持好。 Web API(前后端交互)、配置文件、日志。
XML 文本格式、结构化、可扩展性好,但冗余较大。 Web 服务(SOAP)、旧系统集成、配置文件。
Protobuf / Avro / Thrift 二进制格式、高性能、体积小、有模式定义。 微服务间的高性能通信、大数据存储。
Jackson / Gson 优秀的 JSON 库,提供了强大的注解功能,可以方便地控制序列化/反序列化过程。 Java Web 应用开发的首选。

如何使用 Jackson 进行序列化?

// 添加 Jackson 依赖
// implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JacksonDemo {
    public static void main(String[] args) throws JsonProcessingException {
        User user = new User("Bob", 25, "bobpass");
        // 创建 ObjectMapper 实例
        ObjectMapper mapper = new ObjectMapper();
        // 序列化为 JSON 字符串
        String jsonString = mapper.writeValueAsString(user);
        System.out.println("JSON 序列化结果: " + jsonString);
        // 输出: {"name":"Bob","age":25} (password 字段被忽略)
        // 反序列化为 User 对象
        User fromJson = mapper.readValue(jsonString, User.class);
        System.out.println("JSON 反序列化结果: " + fromJson);
        // 输出: User{name='Bob', age=25, password='null'}
    }
}
特性 Java 原生序列化 JSON / Protobuf 等
实现方式 实现 Serializable 接口,使用 ObjectOutputStream/ObjectInputStream 使用第三方库(如 Jackson, Gson)
数据格式 二进制 JSON (文本) / Protobuf (二进制)
性能 较低 较高 (尤其 Protobuf)
可读性 JSON 好,Protobuf 差
跨语言
安全性 较差(存在反序列化漏洞) 相对较好(需防范注入攻击)
主要用途 RMI、缓存、本地持久化 Web API、微服务通信、大数据

何时使用 Java 原生序列化?

  • 需要将 Java 对象保存到本地磁盘或 RMI(远程方法调用)等纯 Java 环境中。
  • 对性能要求不高,且不涉及跨语言通信。

何时使用 JSON/Protobuf?

  • 现代 Web 应用开发(前后端分离)。
  • 微服务架构中不同语言服务间的通信。
  • 对性能、数据大小和跨平台性有较高要求的场景。
分享:
扫描分享到社交APP
上一篇
下一篇