对象序列化
概述
对象序列化(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
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),通常包装FileOutputStream或ByteArrayOutputStream。 - 写对象:
void writeObject(Object obj),可多次调用写入多个对象;写入顺序与读取顺序需一致。 - 关闭:使用 try-with-resources 管理,确保
close()被调用(会刷写并关闭底层流)。
反序列化:ObjectInputStream
- 构造:
ObjectInputStream(InputStream in),通常包装FileInputStream或ByteArrayInputStream。 - 读对象:
Object readObject()返回下一个序列化对象,需强转为目标类型;若没有更多对象会抛出EOFException。 - 关闭:同样建议 try-with-resources。
说明
writeObject / readObject 会抛出受检异常 IOException(及其子类,如 InvalidClassException),必须捕获或声明抛出。详见 异常处理。
使用示例
示例 1:基本序列化与反序列化
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。显式声明可在一段时期内保持兼容(仅新增字段时,旧数据反序列化时新字段为默认值)。
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)。
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。
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 与序列化无关,但若对象会放入 HashMap、HashSet 等,必须保证 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 按默认值初始化 |
相关链接
- 字节流 — 序列化底层依赖字节流
- 异常处理 — try-with-resources 与 IOException
- NIO 简介 — 更多 IO 与缓冲区概念
- Oracle Java 教程 - 对象序列化