什么是序列化和反序列化?
这是将 Java 对象转换为可以存储或传输的格式,以及从该格式重新构建 Java 对象的过程。

- 序列化:将 Java 对象转换为字节序列的过程,这个字节序列可以保存在文件、数据库中,或者通过网络进行传输。
- 反序列化:将字节序列重新恢复成原来的 Java 对象的过程。
为什么需要它? 想象一下场景:
- 持久化:你想把一个对象(比如一个用户信息
User对象)保存到硬盘上,下次程序启动时再加载回来,直接保存对象是不行的,但序列化成字节流就可以。 - 网络传输:在分布式系统或微服务架构中,服务之间需要传递数据,对象不能直接在网络上传输,但可以被序列化成字节流(如 JSON、XML 或 Java 原生二进制格式)进行传输,接收方再反序列化成对象。
如何实现序列化和反序列化?
Java 提供了非常简单的 API 来实现这个过程,核心是 java.io.ObjectOutputStream 和 java.io.ObjectInputStream。
核心步骤:
-
让类实现
Serializable接口 这是必须的第一步。Serializable是一个标记接口,它里面没有任何方法,它的作用是告诉 JVM:“这个类的对象可以被序列化”,如果一个类没有实现这个接口,尝试序列化它的实例时会抛出NotSerializableException。import java.io.Serializable; public class User implements Serializable { // ... } -
使用
ObjectOutputStream进行序列化 这个类负责将对象写入到输出流中。
(图片来源网络,侵删) -
使用
ObjectInputStream进行反序列化 这个类负责从输入流中读取字节序列并重建对象。
代码示例
下面是一个完整的、可运行的例子。
1 定义可序列化的类
import java.io.Serializable;
// 必须实现 Serializable 接口
public class User implements Serializable {
// 序列化版本号,用于版本控制,强烈建议添加
private static final long serialVersionUID = 1L;
private String name;
private int age;
// transient 关键字修饰的属性不会被序列化
private transient String password;
public User(String name, int age, String password) {
this.name = name;
this.age = age;
this.password = password;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", password='" + (password == null ? "null" : "******") + '\'' + // 出于安全考虑,不显示真实密码
'}';
}
}
关键点解释:
serialVersionUID:这是一个版本控制 ID,当类(User类)的结构发生变化(如增删改字段)时,JVM 会根据类的结构计算出一个新的serialVersionUID,如果反序列化时,文件的serialVersionUID与当前类的serialVersionUID不匹配,就会抛出InvalidClassException,显式声明serialVersionUID可以确保在类结构发生非预期变化时(比如只改了一个注释),版本号依然可控,避免不必要的异常。transient关键字:用transient修饰的成员变量,在进行序列化时会被 JVM 忽略,不会被写入字节流,这在处理一些敏感信息(如密码)或不需要持久化的数据(如数据库连接)时非常有用。
2 执行序列化和反序列化操作
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 filePath 文件路径
*/
public static void serializeObject(Object obj, String filePath) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath))) {
oos.writeObject(obj);
System.out.println("对象已成功序列化到 " + filePath);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 反序列化方法
* @param filePath 文件路径
* @return 反序列化后的对象
*/
public static Object deserializeObject(String filePath) {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath))) {
System.out.println("正在从 " + filePath + " 反序列化对象...");
return ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
}
运行结果:
对象已成功序列化到 user.ser
正在从 user.ser 反序列化对象...
原始对象: User{name='Alice', age=30, password='******'}
反序列化后的对象: User{name='Alice', age=30, password='null'}
结果分析:
name和age被正确序列化和反序列化。password字段因为被transient修饰,在序列化时被忽略,所以反序列化后值为null。
深入理解与注意事项
1 serialVersionUID 的重要性
假设 User 类被修改,增加了一个字段 email:
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 保持不变
private String name;
private int age;
private transient String password;
private String email; // 新增字段
// ...
}
serialVersionUID相同 如果用旧版本的User对象(没有email字段)序列化后的文件,去反序列化新版本的User类(有email字段),JVM 会成功。email字段的值会被赋予默认值(对于 String 是null)。serialVersionUID不同 如果我们没有显式声明serialVersionUID,JVM 会根据类的结构自动生成,增加email字段后,新的serialVersionUID会和旧文件中的不同,此时反序列化会直接抛出InvalidClassException。
始终显式声明 serialVersionUID 是一个好习惯。
.2 静态变量不会被序列化
序列化的是对象的状态(即实例变量),而不是类的状态(静态变量),静态变量属于类,不属于任何实例,因此它不会被序列化。
public class MyClass implements Serializable {
public static int staticVar = 100;
public int instanceVar = 200;
}
序列化一个 MyClass 对象,然后修改 staticVar 的值,再反序列化,你会发现,反序列化后的对象的 staticVar 值是修改后的值,而不是它被序列化时的值。
3 继承关系中的序列化
如果一个类实现了 Serializable,它的所有子类也都默认可序列化,如果父类没有实现 Serializable,子类实现了,那么在序列化子类对象时,父类的字段需要被特殊处理(通过反射调用 AccessibleObject.setAccessible 来强制访问)。
4 安全性考虑
- 数据安全:永远不要序列化包含敏感信息(如密码、密钥)的字段,应该使用
transient关键字。 - 代码安全:反序列化不可信的数据源是极其危险的,攻击者可以构造恶意的字节流,在反序列化时执行任意代码(这被称为“反序列化漏洞”),只反序列化来自可信来源的数据。
序列化的替代方案
虽然 Java 原生的序列化很方便,但它也有一些缺点:
- 性能较差:生成的字节流体积较大,且序列化/反序列化速度相对较慢。
- 跨语言性差:它是 Java 特有的,其他语言(如 Python, C++)无法直接解析。
- 安全问题:如上所述,存在反序列化漏洞的风险。
在现代应用中,尤其是在需要跨语言通信的场景下,通常会选择更通用、更高效的序列化框架:
| 方案 | 特点 | 适用场景 |
|---|---|---|
| JSON | 轻量级、文本格式、人可读、跨语言支持好。 | Web API(前后端交互)、配置文件、日志。 |
| XML | 文本格式、结构化、可扩展性好,但冗余较大。 | Web 服务(SOAP)、旧系统集成、配置文件。 |
| Protobuf / Avro / Thrift | 二进制格式、高性能、体积小、有模式定义。 | 微服务间的高性能通信、大数据存储。 |
| Jackson / Gson | 优秀的 JSON 库,提供了强大的注解功能,可以方便地控制序列化/反序列化过程。 | Java Web 应用开发的首选。 |
如何使用 Jackson 进行序列化?
// 添加 Jackson 依赖
// implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JacksonDemo {
public static void main(String[] args) throws JsonProcessingException {
User user = new User("Bob", 25, "bobpass");
// 创建 ObjectMapper 实例
ObjectMapper mapper = new ObjectMapper();
// 序列化为 JSON 字符串
String jsonString = mapper.writeValueAsString(user);
System.out.println("JSON 序列化结果: " + jsonString);
// 输出: {"name":"Bob","age":25} (password 字段被忽略)
// 反序列化为 User 对象
User fromJson = mapper.readValue(jsonString, User.class);
System.out.println("JSON 反序列化结果: " + fromJson);
// 输出: User{name='Bob', age=25, password='null'}
}
}
| 特性 | Java 原生序列化 | JSON / Protobuf 等 |
|---|---|---|
| 实现方式 | 实现 Serializable 接口,使用 ObjectOutputStream/ObjectInputStream |
使用第三方库(如 Jackson, Gson) |
| 数据格式 | 二进制 | JSON (文本) / Protobuf (二进制) |
| 性能 | 较低 | 较高 (尤其 Protobuf) |
| 可读性 | 差 | JSON 好,Protobuf 差 |
| 跨语言 | 差 | 好 |
| 安全性 | 较差(存在反序列化漏洞) | 相对较好(需防范注入攻击) |
| 主要用途 | RMI、缓存、本地持久化 | Web API、微服务通信、大数据 |
何时使用 Java 原生序列化?
- 需要将 Java 对象保存到本地磁盘或 RMI(远程方法调用)等纯 Java 环境中。
- 对性能要求不高,且不涉及跨语言通信。
何时使用 JSON/Protobuf?
- 现代 Web 应用开发(前后端分离)。
- 微服务架构中不同语言服务间的通信。
- 对性能、数据大小和跨平台性有较高要求的场景。
