什么是序列化和反序列化?
这是一个将对象转换为字节流以及将字节流恢复为对象的过程。

- 序列化:将 Java 对象转换为字节流或其他数据传输格式的过程,这个过程可以让你将对象的状态保存到文件、数据库,或者通过网络传输到另一台机器。
- 反序列化:将字节流或其他数据传输格式恢复为原始 Java 对象的过程,这个过程让你能够从文件、数据库或网络中读取数据,并重新构建出对象。
一个生动的比喻: 想象一下你要把一个复杂的乐高模型(Java 对象)通过邮寄(网络传输)送给朋友。
- 序列化:你把乐高模型拆解成一个个零件(字节流),并打包好(写入文件/网络包)。
- 运输:包裹(字节流)被邮寄给朋友。
- 反序列化:朋友收到包裹后,根据说明书(协议)把零件重新组装起来,得到了一个和原来一模一样的乐高模型(Java 对象)。
为什么需要序列化和反序列化?
序列化机制在 Java 中有着非常广泛和重要的用途:
- 网络传输:当你在进行远程方法调用(如 RMI)或者通过网络发送对象时,对象需要被序列化成字节流才能在网络上传输。
- 持久化存储:你想把一个对象的状态保存到硬盘上,以便程序下次启动时能够恢复它,保存游戏进度、用户会话信息等。
- 深度克隆:通过序列化和反序列化,可以创建一个对象的深拷贝(Deep Copy),这种方法简单且能保证克隆的完整性。
- 跨 JVM 通信:在不同的 Java 虚拟机之间传递对象时,序列化是标准的通信方式。
如何实现序列化和反序列化?(核心步骤)
要让一个 Java 对象可以被序列化,最关键的一步是:
让该类实现 java.io.Serializable 接口。

Serializable 是一个标记接口(Marker Interface),它里面没有任何方法,它的作用仅仅是告诉 JVM:“这个类的对象可以被序列化”。
示例代码
我们创建一个 Student 类,并让它实现 Serializable 接口。
import java.io.Serializable;
// 1. 实现 Serializable 接口
public class Student implements Serializable {
// 2. 建议添加一个序列化版本号 UID (SerialVersionUID)
private static final long serialVersionUID = 1L;
private String name;
private int age;
private transient String password; // transient 关键字见下文
public Student(String name, int age, String password) {
this.name = name;
this.age = age;
this.password = password;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", password='" + password + '\'' +
'}';
}
}
代码解释:
implements Serializable:这是必须的。serialVersionUID:这是一个非常重要的字段,它相当于对象的“身份证号”,在进行反序列化时,JVM 会比较字节流中的serialVersionUID和本地类的serialVersionUID,如果两者不一致,会抛出InvalidClassException。强烈建议在所有可序列化的类中显式定义serialVersionUID,这样可以避免因类结构(如修改字段、增删方法)的微小变动导致反序列化失败。transient关键字:用transient修饰的字段不会被序列化,这在处理敏感信息(如密码)或不需要持久化的数据时非常有用,上面的password字段就不会被保存到文件中。
序列化和反序列化的 API
Java 提供了两个核心类来处理这个过程:

ObjectOutputStream:用于将对象写入到输出流(如文件、网络)。ObjectInputStream:用于从输入流(如文件、网络)中读取对象并重建。
完整示例
我们将一个 Student 对象序列化到 student.ser 文件中,然后再从该文件中反序列化出来。
import java.io.*;
public class SerializationDemo {
public static void main(String[] args) {
// 创建一个 Student 对象
Student originalStudent = new Student("Alice", 20, "123456");
// --- 1. 序列化过程 ---
// try-with-resources 语句,确保流被自动关闭
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.ser"))) {
oos.writeObject(originalStudent);
System.out.println("对象已成功序列化到 student.ser 文件。");
} catch (IOException e) {
e.printStackTrace();
}
// --- 2. 反序列化过程 ---
Student deserializedStudent = null;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.ser"))) {
// 从文件中读取对象
deserializedStudent = (Student) ois.readObject();
System.out.println("对象已成功从 student.ser 文件反序列化。");
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
// --- 3. 验证结果 ---
if (deserializedStudent != null) {
System.out.println("原始对象: " + originalStudent);
System.out.println("反序列化对象: " + deserializedStudent);
// 注意:password 字段是 transient,所以为 null
System.out.println("反序列化后的密码: " + deserializedStudent.getPassword());
}
}
}
输出结果:
对象已成功序列化到 student.ser 文件。
对象已成功从 student.ser 文件反序列化。
原始对象: Student{name='Alice', age=20, password='123456'}
反序列化对象: Student{name='Alice', age=20, password='null'}
反序列化后的密码: null
可以看到,反序列化得到的对象除了 transient 字段 password 为 null 外,其他信息都完美恢复了。
高级话题和注意事项
1 serialVersionUID 的作用详解
- 显式声明:如上所述,推荐显式声明。
- 隐式生成:如果你没有显式声明,JVM 会在编译时根据类的结构(类名、接口、字段、方法等)自动计算一个
serialVersionUID。这个计算过程对编译器的实现细节非常敏感,一旦你对类做了任何微小的改动(比如增加一个字段、修改一个方法的访问修饰符),JVM 重新计算出的serialVersionUID就会不同,导致反序列化时抛出InvalidClassException。
2 Externalizable 接口
Externalizable 是 Serializable 的子接口,它提供了更高级的控制。
Serializable:JVM 自动完成序列化过程,你只需要声明类可序列化即可,这种方式简单,但性能开销较大,因为它会递归地序列化对象图中的所有对象。Externalizable:你需要手动实现writeExternal(ObjectOutput out)和readExternal(ObjectInput in)方法来控制序列化和反序列化的具体过程,这提供了更高的性能和灵活性,但你需要自己处理所有字段的读写,并且要负责处理父类的序列化。
使用场景:当你对性能要求极高,或者需要以特定格式(如 JSON、XML)序列化对象时,可以考虑 Externalizable。
3 继承与序列化
- 如果一个类实现了
Serializable,那么它的所有子类也都默认是可序列化的。 - 父类如果没有实现
Serializable,但子类实现了,那么在序列化子类对象时,父类的字段需要通过反射机制来调用无参构造函数进行初始化,如果父类没有无参构造函数,就会抛出InvalidClassException。
4 安全性考虑
序列化/反序列化过程可能带来安全风险,特别是当从不受信任的来源(如网络)接收字节流时。
- 反序列化漏洞:攻击者可以精心构造一个恶意的字节流,其中包含一个指向特定类(如
java.lang.Runnable的恶意实现)的引用,当目标程序反序列化这个字节流时,会尝试加载并执行这个恶意类,可能导致远程代码执行。 - 防御措施:
- 不信任输入:永远不要反序列化来自不可信来源的数据。
- 使用白名单:在反序列化之前,检查要加载的类是否在你的安全白名单中。
- 使用替代方案:对于需要传输的数据,优先使用 JSON、XML 等文本格式,而不是二进制的 Java 序列化,这些格式更安全、更通用。
现代替代方案
传统的 Java 序列化虽然强大,但也存在一些缺点(如性能差、不安全、跨语言性差),在现代应用中,我们更倾向于使用以下替代方案:
| 方案 | 格式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| JSON | 文本 | 轻量级、可读性好、跨语言、有成熟的库(如 Jackson, Gson) | 需要手动定义转换逻辑或使用注解 | Web API (RESTful)、配置文件、日志 |
| XML | 文本 | 可读性好、支持命名空间和复杂结构 | 冗长、解析速度慢 | 企业级应用配置、Web 服务 |
| Protocol Buffers / gRPC | 二进制 | 高性能、高效率、强类型、跨语言 | 需要先定义 .proto 文件并编译生成代码 |
微服务、高性能 RPC 通信 |
| Kryo | 二进制 | 非常快,比 Java 原生序列化性能高得多 | 不是标准库,依赖第三方 | 游戏、大数据处理、高性能计算 |
- 如果你在 JVM 之间传递对象,且对性能和安全性要求不高,传统的
Serializable仍然可用。 - 如果你在构建 Web 服务或需要跨语言交互,JSON 是事实上的标准。
- 如果你追求极致的性能,Protocol Buffers 或 Kryo 是更好的选择。
| 特性 | 描述 |
|---|---|
| 核心接口 | java.io.Serializable (标记接口) |
| 核心类 | ObjectOutputStream (序列化), ObjectInputStream (反序列化) |
| 关键步骤 | 类实现 Serializable。使用 ObjectOutputStream.write()。使用 ObjectInputStream.read()。 |
| 重要字段 | serialVersionUID:用于版本控制,防止反序列化失败。 |
| 关键字 | transient:标记不被序列化的字段。 |
| 高级接口 | Externalizable:提供手动控制序列化过程的能力,性能更高。 |
| 安全风险 | 反序列化可能被利用进行远程代码攻击。 |
| 现代替代 | JSON, Protocol Buffers, Kryo 等,它们更安全、高效和通用。 |
