什么是序列化和反序列化?
它们是将对象转换为字节流和将字节流恢复为对象的过程。

- 序列化:将 Java 对象(及其状态)转换成一种字节序列(或称为二进制流)的过程,这个字节序列可以保存在文件、数据库中,或者通过网络传输。
- 反序列化:将这个字节序列重新恢复成原来的 Java 对象的过程。
为什么需要它? Java 的程序运行在内存中,而内存是易失的,当程序关闭或服务器重启时,内存中的所有对象都会丢失,序列化提供了一种机制,可以将对象的状态持久化,以便在之后可以重新创建它,它也是 Java RMI(远程方法调用)、Java 消息队列等技术的基石。
如何实现序列化和反序列化?
Java 提供了一套内置的 API 来实现这个过程,核心是 java.io.Serializable 接口和 ObjectOutputStream / ObjectInputStream 类。
核心概念:Serializable 接口
一个类要想被序列化,必须实现 java.io.Serializable 接口,这个接口是一个标记接口,它没有任何方法或字段,只是起到一个标识作用,告诉 JVM:“这个类的对象可以被序列化”。
import java.io.Serializable;
// 实现了 Serializable 接口,这个类的对象就可以被序列化了
public class User implements Serializable {
// ... 类定义 ...
}
核心类:ObjectOutputStream 和 ObjectInputStream
ObjectOutputStream:负责将对象序列化成字节流。writeObject(Object obj):将指定的对象写入输出流。
ObjectInputStream:负责从字节流反序列化成对象。readObject():从输入流中读取一个对象。
完整示例代码
我们通过一个完整的例子来演示整个过程。

第1步:创建一个可序列化的 User 类
import java.io.Serializable;
// 必须实现 Serializable 接口
public class User implements Serializable {
// 序列化版本号,强烈推荐添加,后面会解释为什么
private static final long serialVersionUID = 1L;
private String name;
private int age;
private transient String password; // 使用 transient 关键字
// 构造方法
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 int getAge() { return age; }
public String getPassword() { return password; }
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", password='" + (password != null ? "******" : "null") + '\'' +
'}';
}
}
注意:
serialVersionUID:这是一个唯一的版本标识符,当类结构(如增删改字段)发生变化时,JVM 会根据类的结构计算出一个新的serialVersionUID,如果反序列化时,文件的serialVersionUID与当前类的serialVersionUID不匹配,就会抛出InvalidClassException,手动定义它可以避免因类结构微小变动(比如增加一个无关紧要的字段)导致的序列化/反序列化失败。transient关键字:用transient修饰的字段不会被序列化,对于一些敏感信息(如密码)或不需要持久化的状态(如Thread对象),我们会使用transient,反序列化后,这些字段的值会被设为对应类型的默认值(如null,0,false)。
第2步:编写序列化和反序列化的代码
import java.io.*;
public class SerializationDemo {
public static void main(String[] args) {
// 1. 创建一个 User 对象
User user = new User("Alice", 30, "123456");
// --- 序列化过程:将对象写入文件 ---
String fileName = "user.ser";
serializeObject(user, fileName);
// --- 反序列化过程:从文件中读取对象 ---
User deserializedUser = deserializeObject(fileName);
// 3. 验证结果
System.out.println("原始对象: " + user);
System.out.println("反序列化后的对象: " + deserializedUser);
System.out.println("两个对象是否相等: " + user.equals(deserializedUser)); // 注意:这里比较的是地址,实际可能需要重写 equals
}
/**
* 序列化对象到文件
* @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 User deserializeObject(String fileName) {
User user = null;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName))) {
user = (User) ois.readObject();
System.out.println("对象已成功从文件反序列化: " + fileName);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return user;
}
}
运行结果:
对象已成功序列化到文件: user.ser
对象已成功从文件反序列化: user.ser
原始对象: User{name='Alice', age=30, password='******'}
反序列化后的对象: User{name='Alice', age=30, password='null'}
两个对象是否相等: false
从结果可以看出,password 字段因为被 transient 修饰,在反序列化后变成了 null。
序列化的高级特性
1 serialVersionUID 的作用详解
serialVersionUID 是序列化机制中的“身份证号”。
- 显式声明:如示例中所示,
private static final long serialVersionUID = 1L; - 隐式生成:如果没有显式声明,JVM 会在编译时根据类的结构(字段、方法、接口等)自动生成一个,这个生成过程依赖于编译器的实现,并且在不同版本(如不同 JDK)或不同编译器下可能不同。
为什么必须要有?
假设你序列化了一个 User 对象到文件,然后修改了 User 类,比如增加了一个 String email 字段,现在你尝试用修改后的类去反序列化那个文件,会发生什么?
- 没有
serialVersionUID:JVM 会发现文件中的类结构和当前类的结构不一致(文件里没有email字段,当前类有),于是抛出InvalidClassException。 - 有
serialVersionUID:只要类的核心逻辑没有发生根本性变化(比如删除了父类或实现了新的接口),即使增加了新字段,只要serialVersionUID相同,JVM 就会尝试兼容,它会将文件中已有的字段正确赋值,而新增的字段会被设为默认值,这提供了极大的灵活性。
2 writeObject 和 readObject 方法
如果你想在序列化或反序列化时执行一些自定义逻辑,可以在类中定义 private 的 writeObject 和 readObject 方法。
import java.io.*;
public class CustomSerialization implements Serializable {
private String data;
private transient String secret;
public CustomSerialization(String data, String secret) {
this.data = data;
this.secret = secret;
}
/**
* 自定义序列化逻辑
* @param oos ObjectOutputStream
* @throws IOException
*/
private void writeObject(ObjectOutputStream oos) throws IOException {
// 默认序列化逻辑
oos.defaultWriteObject();
// 自定义逻辑:将 transient 字段手动写入
oos.writeObject("SECRET_PREFIX_" + secret);
}
/**
* 自定义反序列化逻辑
* @param ois ObjectInputStream
* @throws IOException
* @throws ClassNotFoundException
*/
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
// 默认反序列化逻辑
ois.defaultReadObject();
// 自定义逻辑:手动读取 transient 字段
String prefixedSecret = (String) ois.readObject();
this.secret = prefixedSecret.replace("SECRET_PREFIX_", "");
}
@Override
public String toString() {
return "CustomSerialization{" +
"data='" + data + '\'' +
", secret='" + secret + '\'' +
'}';
}
}
在这个例子中,即使 secret 是 transient 的,我们也通过自定义方法将其序列化了,并且在反序列化时恢复了它。
序列化的替代方案
虽然 Serializable 很方便,但它也有一些缺点,
- 性能开销:反射机制导致性能较低。
- 安全性问题:反序列化可能被恶意利用,导致远程代码执行漏洞(详见下文)。
- 非侵入性:需要修改类源码来实现接口,不够灵活。
在现代 Java 开发中,有几种更优的替代方案:
1 JSON / XML 格式
这是目前最主流的方式,将对象转换为文本格式的 JSON 或 XML。
- 优点:
- 可读性强:人类可以读懂,便于调试。
- 跨语言:几乎所有编程语言都支持 JSON 和 XML。
- 安全性高:没有反序列化漏洞的风险。
- 灵活性高:不需要实现任何接口。
- 常用库:
- JSON: Jackson, Gson, Fastjson (阿里巴巴)。
- XML: JAXB (Java 内置), XStream。
示例 (使用 Jackson):
// 添加 Jackson 依赖
// implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0'
import com.fasterxml.jackson.databind.ObjectMapper;
public class JsonSerializationDemo {
public static void main(String[] args) throws Exception {
User user = new User("Bob", 25, "654321");
// 1. 序列化成 JSON 字符串
ObjectMapper mapper = new ObjectMapper();
String jsonString = mapper.writeValueAsString(user);
System.out.println("JSON 序列化结果: " + jsonString);
// 2. 反序列化成对象
User fromJson = mapper.readValue(jsonString, User.class);
System.out.println("JSON 反序列化结果: " + fromJson);
}
}
2 Java 外部化接口 (Externalizable)
Externalizable 是 Serializable 的子接口,提供了更细粒度的控制。
- 区别:
Serializable:采用默认机制,自动序列化所有非transient字段。Externalizable:需要手动实现writeExternal()和readExternal()方法,完全由你来控制哪些字段被序列化以及如何序列化。
- 优点:性能更高,控制力更强。
- 缺点:代码更繁琐,需要自己处理所有字段。
安全风险:反序列化漏洞
这是一个非常重要的安全问题,当反序列化不可信的数据源(如网络请求、用户上传的文件)时,攻击者可以构造恶意的字节流,使其在反序列化时执行任意代码。
攻击原理:
攻击者可以创建一个包含恶意代码的类(MaliciousClass),并重写其 readObject() 方法,然后将这个类的对象序列化成字节流发送给你的程序,当你的程序调用 ObjectInputStream.readObject() 时,JVM 会调用 MaliciousClass 的 readObject() 方法,从而执行其中的恶意代码。
防御措施:
- 不要反序列化不可信的数据:这是最根本的原则。
- 使用白名单:只允许反序列化已知的、安全的类。
- 使用安全的替代方案:优先使用 JSON 等文本格式。
- 使用安全库:如 SerialKiller 可以在反序列化前检查类的黑/白名单。
- 保持库更新:许多序列化库(如 Jackson)会及时修复已知的安全漏洞。
| 特性 | Serializable |
Externalizable |
JSON / XML |
|---|---|---|---|
| 实现方式 | 实现接口,无需编写代码 | 实现接口,需手动编写 writeExternal/ReadExternal |
使用第三方库,无需修改类 |
| 控制力 | 低(自动序列化所有非 transient 字段) |
高(完全手动控制) | 高(通过注解或配置控制) |
| 性能 | 较低(使用反射) | 较高(不使用反射) | 较高(解析文本) |
| 可读性 | 二进制,不可读 | 二进制,不可读 | 文本,可读 |
| 跨语言 | 否 | 否 | 是 |
| 安全性 | 较低(有反序列化漏洞风险) | 较低(同样有风险) | 高(无漏洞风险) |
| 使用场景 | 简单的、内部对象的持久化(如 RMI) | 对性能有极高要求的场景 | Web API、配置文件、跨系统通信等现代场景 |
建议:
- 对于内部系统、简单、对性能要求不高的对象,如果必须使用二进制序列化,
Serializable是一个快速的选择。 - 对于Web 服务、API、跨语言通信、任何涉及用户输入或网络传输的场景,强烈推荐使用 JSON 等文本格式,它更安全、更灵活、更通用。
