什么是序列化和反序列化?
它们是对象和字节流之间的一对转换过程。
- 序列化:将 Java 对象转换成字节序列的过程,这个字节序列可以保存在文件中,或者通过网络传输到另一个地方。
- 反序列化:将字节序列恢复成原来的 Java 对象的过程。
为什么需要它? Java 的程序运行在内存中,而对象是存在于内存中的,内存是易失的(程序关闭后数据就丢失了),如果我们想将对象的状态(即它的成员变量的值)持久化保存下来(比如存到硬盘),或者想在网络间传递对象,就需要一种方式将对象从内存中“移出”和“移入”,序列化和反序列化就是实现这个功能的机制。
如何实现序列化和反序列化?
Java 提供了一套非常简单的 API 来实现这个功能,核心步骤如下:
让类实现 Serializable 接口
这是最关键的一步,一个类要想被序列化,必须实现 java.io.Serializable 接口。
重要提示:Serializable 是一个标记接口,它内部没有任何方法,它的作用只是告诉 JVM:“这个类的对象可以被序列化”,如果尝试序列化一个没有实现 Serializable 接口的对象,JVM 会抛出 NotSerializableException。
import java.io.Serializable;
public class User implements Serializable {
// ...
}
使用 ObjectOutputStream 进行序列化
ObjectOutputStream 是一个高级输出流,它可以将一个对象写入到输出流中。
核心方法:
writeObject(Object obj): 将指定的对象写入输出流。
使用 ObjectInputStream 进行反序列化
ObjectInputStream 是一个高级输入流,它可以从输入流中读取一个对象。
核心方法:
readObject(): 从输入流中读取一个对象。
完整代码示例
下面是一个完整的例子,演示如何序列化一个 User 对象到文件,然后再从文件中反序列化回来。
定义可序列化的 User 类
import java.io.Serializable;
// 必须实现 Serializable 接口
public class User implements Serializable {
// 为了保证版本兼容性,强烈建议添加此ID
private static final long serialVersionUID = 1L;
private String name;
private int age;
private String password; // 注意:敏感信息不应序列化
// 构造方法
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 void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", password='" + password + '\'' +
'}';
}
}
编写序列化和反序列化的主程序
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 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 Object deserializeObject(String fileName) {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName))) {
Object obj = ois.readObject();
System.out.println("对象已成功从文件反序列化: " + fileName);
return obj;
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
}
运行结果:
对象已成功序列化到文件: user.ser
对象已成功从文件反序列化: user.ser
原始对象: User{name='Alice', age=30, password='123456'}
反序列化后的对象: User{name='Alice', age=30, password='123456'}
重要概念和注意事项
serialVersionUID - 版本控制
serialVersionUID 是一个唯一的标识符,用于在反序列化时验证序列化对象的版本和当前类的版本是否兼容。
- 如果没有显式定义:JVM 会根据类的结构(如类名、接口、字段、方法等)自动计算一个
serialVersionUID。强烈不推荐这样做,因为一旦你修改了类(比如增加了一个字段),这个自动计算的 ID 就会改变,导致反序列化时抛出InvalidClassException。 - 如果显式定义:当你修改类结构时,可以控制
serialVersionUID,如果它和序列化文件中的 ID 不匹配,则会抛出InvalidClassException,这给了你明确的控制权。
最佳实践:始终为你每一个可序列化的类显式声明一个 private static final long serialVersionUID。
private static final long serialVersionUID = L; // L 是一个任意长整数值
transient 关键字 - 排除字段
如果一个你不希望被序列化的字段(敏感信息如密码、临时计算值、或依赖于特定 JVM 状态的对象),可以使用 transient 关键字来标记它。
被 transient 修饰的字段在序列化时会被忽略,反序列化后,该字段的值会被设为对应数据类型的默认值(如 int 为 0,boolean 为 false,对象引用为 null)。
修改 User 类示例:
public class User implements Serializable {
// ...
private transient String password; // password 不会被序列化
// ...
}
运行结果变化:
反序列化后的对象会显示:User{name='Alice', age=30, password='null'}
序列化安全风险
序列化并不安全,攻击者可以构造恶意的字节流,当你的程序尝试反序列化它时,可能会执行任意代码(这被称为 "反序列化漏洞")。永远不要反序列化来自不可信来源的数据。
自定义序列化过程
如果你需要更精细地控制序列化和反序列化的过程(加密数据、处理 transient 字段的默认值等),可以提供以下两个特殊方法:
private void writeObject(ObjectOutputStream out) throws IOExceptionprivate void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
JVM 在序列化和反序列化时会自动检查并调用这些方法(如果它们存在)。
示例:为 transient 密码设置默认值
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 执行默认的反序列化过程
// 为 transient 字段 password 设置一个默认值
this.password = "default_password";
}
现代替代方案
虽然 Java 原生的序列化 API 很方便,但它也存在一些问题,比如性能较差、不安全、跨语言支持差等,在现代应用开发中,通常会使用更高效、更安全的替代方案:
-
JSON:
- 优点: 跨语言、可读性好、是人类可读的文本格式,是目前最流行的数据交换格式。
- 常用库:
Jackson,Gson,Fastjson(阿里巴巴)。 - 场景: Web API 前后端数据交互、配置文件等。
-
XML:
- 优点: 可读性好、有严格的规范。
- 缺点: 冗余信息多,解析性能不如 JSON。
- 场景: 旧系统集成、企业级应用配置。
-
Protocol Buffers (Protobuf):
- 优点: 性能极高、数据体积小、代码生成、强类型、跨语言。
- 缺点: 二进制格式,不可读;需要
.proto文件定义结构。 - 场景: 微服务间通信、高性能系统。
-
Kryo:
- 优点: 专门为 Java 设计,性能极高,序列化后体积小。
- 缺点: 主要用于 Java 生态,跨语言支持不如 Protobuf。
- 场景: 需要在 Java 应用中高性能地序列化对象(如缓存、RPC 框架)。
| 特性 | Java 原生序列化 | JSON / Protobuf / Kryo |
|---|---|---|
| 易用性 | 非常高,只需实现接口 | 中等,需要引入库和定义结构 |
| 性能 | 较差 | 高 (Protobuf/Kryo) / 中等 (JSON) |
| 可读性 | 差 (二进制) | 高 (JSON) / 差 (Protobuf/Kryo) |
| 安全性 | 低,存在反序列化漏洞 | 高,特别是 Protobuf/Kryo |
| 跨语言 | 差 | 好 (JSON/Protobuf) / 差 (Kryo) |
| 主要用途 | RMI、缓存、简单的对象持久化 | Web API、微服务、高性能数据交换 |
- 如果只是简单的 Java 应用内部对象持久化或 RMI 通信,原生序列化足够用。
- 对于任何需要跨系统、跨语言交换数据,或者对性能、安全性有要求的现代应用,强烈推荐使用 JSON 或 Protocol Buffers 等现代方案。
