Skip to content

对象序列化

概述

对象序列化(Object Serialization)指将 Java 对象转换为字节流,以便写入文件、数据库或通过网络传输;反序列化则是将字节流还原为对象。Java 通过 java.io.Serializable 接口与 ObjectOutputStream/ObjectInputStream 实现这一机制,常用于持久化、RPC、缓存等场景。

理解序列化有助于你正确保存和恢复对象状态,并避免版本变更导致的 InvalidClassException 等常见问题。序列化建立在 字节流 之上,建议先掌握 InputStream/OutputStream 的基本用法。


序列化机制概览

概念说明
Serializable标记接口,实现该接口的类才能被序列化
ObjectOutputStream将对象写入底层输出流(如文件、网络)
ObjectInputStream从底层输入流读取并还原对象
serialVersionUID类的序列化版本号,用于反序列化时校验兼容性
transient修饰的成员变量不参与序列化
  • 可序列化条件:类实现 Serializable;其非 transient、非 static 的成员要么是基本类型/包装类/String,要么本身可序列化。
  • 不参与序列化static 变量、transient 变量;未实现 Serializable 的引用类型若不标为 transient,会抛出 NotSerializableException

提示

若仅需跨语言或前后端交换数据,可考虑 JSON(如 Jackson、Gson)等格式,而非 Java 原生序列化。Java 序列化更适合 JVM 内部或纯 Java 之间的对象持久化与传输。


基本用法

实现 Serializable

java
import java.io.Serializable;

// 实现 Serializable 即可被 ObjectOutputStream 序列化
public class User implements Serializable {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
}

序列化:ObjectOutputStream

  • 构造ObjectOutputStream(OutputStream out),通常包装 FileOutputStreamByteArrayOutputStream
  • 写对象void writeObject(Object obj),可多次调用写入多个对象;写入顺序与读取顺序需一致。
  • 关闭:使用 try-with-resources 管理,确保 close() 被调用(会刷写并关闭底层流)。

反序列化:ObjectInputStream

  • 构造ObjectInputStream(InputStream in),通常包装 FileInputStreamByteArrayInputStream
  • 读对象Object readObject() 返回下一个序列化对象,需强转为目标类型;若没有更多对象会抛出 EOFException
  • 关闭:同样建议 try-with-resources。

说明

writeObject / readObject 会抛出受检异常 IOException(及其子类,如 InvalidClassException),必须捕获或声明抛出。详见 异常处理


使用示例

示例 1:基本序列化与反序列化

java
import java.io.*;

public class SerializationBasic {
    public static void main(String[] args) {
        User user = new User("张三", 25);

        // 序列化:将对象写入文件
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"))) {
            oos.writeObject(user);
        } catch (IOException e) {
            System.err.println("序列化失败: " + e.getMessage());
            return;
        }

        // 反序列化:从文件读回对象
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"))) {
            User restored = (User) ois.readObject();
            System.out.println(restored.getName() + ", " + restored.getAge()); // 张三, 25
        } catch (IOException | ClassNotFoundException e) {
            System.err.println("反序列化失败: " + e.getMessage());
        }
    }
}

示例 2:serialVersionUID 与版本兼容

若类未显式声明 serialVersionUID,JDK 会根据类结构自动生成。一旦你修改了类(如增删字段、改方法签名),自动生成的 UID 会变,旧数据反序列化时可能抛出 InvalidClassException显式声明可在一段时期内保持兼容(仅新增字段时,旧数据反序列化时新字段为默认值)。

java
public class User implements Serializable {
    // 显式声明版本号,类做兼容性修改时可保持不变,避免旧数据无法反序列化
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    // 新增字段:反序列化旧文件时,email 为 null
    private String email;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // getters / setters
}

示例 3:transient 与不参与序列化的字段

密码、线程、连接等不应或无法序列化的字段,应使用 transient 修饰;反序列化后这些字段为默认值(对象引用为 null,数值为 0/false)。

java
public class Account implements Serializable {
    private static final long serialVersionUID = 1L;

    private String username;
    private transient String password;  // 不写入字节流,反序列化后为 null
    private transient Thread worker;    // Thread 未实现 Serializable,若不标 transient 会报错

    public Account(String username, String password) {
        this.username = username;
        this.password = password;
    }
    // 反序列化后若需使用 password,应由其他途径(如重新从安全存储读取)赋值
}

示例 4:写入与读取多个对象

写入顺序与读取顺序必须一致;可用循环或约定好的顺序多次 writeObject / readObject

java
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users.dat"))) {
    oos.writeObject(new User("A", 20));
    oos.writeObject(new User("B", 22));
}

try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("users.dat"))) {
    User u1 = (User) ois.readObject();
    User u2 = (User) ois.readObject();
    System.out.println(u1.getName() + ", " + u2.getName()); // A, B
}

注意事项

注意

重写 equals/hashCode 与序列化无关,但若对象会放入 HashMapHashSet 等,必须保证 equals/hashCode 契约;参与 equals/hashCode 计算的字段若被标为 transient,反序列化后该字段为默认值,可能导致与「序列化前」的对象在集合中行为不一致。若此类字段不应序列化,应在反序列化后重新计算或从别处恢复。详见 常用类

注意

安全:不要对不可信来源的字节流直接做 ObjectInputStream#readObject()。Java 反序列化会执行类中的逻辑,历史上存在大量反序列化漏洞。来自网络或外部文件的「对象流」应视为高风险,优先使用 JSON、Protocol Buffers 等格式并做校验。

提示

  • 显式声明 serialVersionUID:便于做兼容性演进(如只新增可选字段),避免因类结构变化导致旧数据无法反序列化。
  • 敏感字段用 transient:密码、密钥等不要写入序列化流。
  • 优先 try-with-resources:确保 ObjectOutputStream/ObjectInputStream 及其底层流被正确关闭。
常见问题原因与处理
NotSerializableException某成员类型未实现 Serializable,且未标为 transient
InvalidClassException类结构变化导致 serialVersionUID 与写入时不一致;显式 UID 并做兼容修改可缓解
反序列化后部分字段为 null/0该字段被 transient 或为 static,不参与序列化
子类可序列化、父类不可序列化父类需有无参构造方法,反序列化时父类字段由 JVM 按默认值初始化

相关链接

基于 VitePress 构建