杰瑞科技汇

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

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

它们是对象和字节流之间的一对转换过程。

  • 序列化:将 Java 对象转换成字节序列的过程,这个字节序列可以保存在文件中,或者通过网络传输到另一个地方。
  • 反序列化:将字节序列恢复成原来的 Java 对象的过程。

为什么需要它? Java 的程序运行在内存中,而对象是存在于内存中的,内存是易失的(程序关闭后数据就丢失了),如果我们想将对象的状态(即它的成员变量的值)持久化保存下来(比如存到硬盘),或者想在网络间传递对象,就需要一种方式将对象从内存中“移出”和“移入”,序列化和反序列化就是实现这个功能的机制。


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

Java 提供了一套非常简单的 API 来实现这个功能,核心步骤如下:

让类实现 Serializable 接口

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

重要提示Serializable 是一个标记接口,它内部没有任何方法,它的作用只是告诉 JVM:“这个类的对象可以被序列化”,如果尝试序列化一个没有实现 Serializable 接口的对象,JVM 会抛出 NotSerializableException

import java.io.Serializable;
public class User implements Serializable {
    // ...
}

使用 ObjectOutputStream 进行序列化

ObjectOutputStream 是一个高级输出流,它可以将一个对象写入到输出流中。

核心方法

  • writeObject(Object obj): 将指定的对象写入输出流。

使用 ObjectInputStream 进行反序列化

ObjectInputStream 是一个高级输入流,它可以从输入流中读取一个对象。

核心方法

  • readObject(): 从输入流中读取一个对象。

完整代码示例

下面是一个完整的例子,演示如何序列化一个 User 对象到文件,然后再从文件中反序列化回来。

定义可序列化的 User

import java.io.Serializable;
// 必须实现 Serializable 接口
public class User implements Serializable {
    // 为了保证版本兼容性,强烈建议添加此ID
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    private String password; // 注意:敏感信息不应序列化
    // 构造方法
    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 void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", password='" + password + '\'' +
                '}';
    }
}

编写序列化和反序列化的主程序

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 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 Object deserializeObject(String fileName) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName))) {
            Object obj = ois.readObject();
            System.out.println("对象已成功从文件反序列化: " + fileName);
            return obj;
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }
}

运行结果:

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

重要概念和注意事项

serialVersionUID - 版本控制

serialVersionUID 是一个唯一的标识符,用于在反序列化时验证序列化对象的版本和当前类的版本是否兼容。

  • 如果没有显式定义:JVM 会根据类的结构(如类名、接口、字段、方法等)自动计算一个 serialVersionUID强烈不推荐这样做,因为一旦你修改了类(比如增加了一个字段),这个自动计算的 ID 就会改变,导致反序列化时抛出 InvalidClassException
  • 如果显式定义:当你修改类结构时,可以控制 serialVersionUID,如果它和序列化文件中的 ID 不匹配,则会抛出 InvalidClassException,这给了你明确的控制权。

最佳实践:始终为你每一个可序列化的类显式声明一个 private static final long serialVersionUID

private static final long serialVersionUID = L; // L 是一个任意长整数值

transient 关键字 - 排除字段

如果一个你不希望被序列化的字段(敏感信息如密码、临时计算值、或依赖于特定 JVM 状态的对象),可以使用 transient 关键字来标记它。

transient 修饰的字段在序列化时会被忽略,反序列化后,该字段的值会被设为对应数据类型的默认值(如 int0booleanfalse,对象引用为 null)。

修改 User 类示例:

public class User implements Serializable {
    // ...
    private transient String password; // password 不会被序列化
    // ...
}

运行结果变化:

反序列化后的对象会显示:User{name='Alice', age=30, password='null'}

序列化安全风险

序列化并不安全,攻击者可以构造恶意的字节流,当你的程序尝试反序列化它时,可能会执行任意代码(这被称为 "反序列化漏洞")。永远不要反序列化来自不可信来源的数据

自定义序列化过程

如果你需要更精细地控制序列化和反序列化的过程(加密数据、处理 transient 字段的默认值等),可以提供以下两个特殊方法:

  • private void writeObject(ObjectOutputStream out) throws IOException
  • private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException

JVM 在序列化和反序列化时会自动检查并调用这些方法(如果它们存在)。

示例:为 transient 密码设置默认值

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject(); // 执行默认的反序列化过程
    // 为 transient 字段 password 设置一个默认值
    this.password = "default_password";
}

现代替代方案

虽然 Java 原生的序列化 API 很方便,但它也存在一些问题,比如性能较差、不安全、跨语言支持差等,在现代应用开发中,通常会使用更高效、更安全的替代方案:

  1. JSON:

    • 优点: 跨语言、可读性好、是人类可读的文本格式,是目前最流行的数据交换格式。
    • 常用库: Jackson, Gson, Fastjson (阿里巴巴)。
    • 场景: Web API 前后端数据交互、配置文件等。
  2. XML:

    • 优点: 可读性好、有严格的规范。
    • 缺点: 冗余信息多,解析性能不如 JSON。
    • 场景: 旧系统集成、企业级应用配置。
  3. Protocol Buffers (Protobuf):

    • 优点: 性能极高、数据体积小、代码生成、强类型、跨语言。
    • 缺点: 二进制格式,不可读;需要 .proto 文件定义结构。
    • 场景: 微服务间通信、高性能系统。
  4. Kryo:

    • 优点: 专门为 Java 设计,性能极高,序列化后体积小。
    • 缺点: 主要用于 Java 生态,跨语言支持不如 Protobuf。
    • 场景: 需要在 Java 应用中高性能地序列化对象(如缓存、RPC 框架)。
特性 Java 原生序列化 JSON / Protobuf / Kryo
易用性 非常高,只需实现接口 中等,需要引入库和定义结构
性能 较差 (Protobuf/Kryo) / 中等 (JSON)
可读性 差 (二进制) (JSON) / 差 (Protobuf/Kryo)
安全性 ,存在反序列化漏洞 ,特别是 Protobuf/Kryo
跨语言 (JSON/Protobuf) / 差 (Kryo)
主要用途 RMI、缓存、简单的对象持久化 Web API、微服务、高性能数据交换
  • 如果只是简单的 Java 应用内部对象持久化或 RMI 通信,原生序列化足够用。
  • 对于任何需要跨系统、跨语言交换数据,或者对性能、安全性有要求的现代应用,强烈推荐使用 JSON 或 Protocol Buffers 等现代方案
分享:
扫描分享到社交APP
上一篇
下一篇