Java 序列化
序列化与反序列化
前置知识
- Java Path 与 Files 语法速查手册:建议先完成前一篇的学习
学习目标
- 掌握「0. 本节阅读指引(先读这一节)」的核心机制、典型用法与常见陷阱
- 掌握「概述」的核心机制、典型用法与常见陷阱
- 掌握「基础概念」的核心机制、典型用法与常见陷阱
- 掌握「快速上手」的核心机制、典型用法与常见陷阱
- 掌握「详细用法」的核心机制、典型用法与常见陷阱
0. 本节阅读指引(先读这一节)
本篇是「序列化」,语法与实践并重。
第一遍只读:概述、基础概念、快速上手、详细用法与 Serializable、transient、对象流序列化核心小节;会实现 Serializable 并读写对象。
可跳过:进阶用法(Jackson、Gson、ProtoBuf)按需查阅。
前置:026 I/O 流与文件操作。
概述
序列化是将 Java 对象转换为字节流的过程,反序列化是将字节流恢复为 Java 对象的过程。序列化的核心用途是让对象可以脱离内存存在:存储到文件、通过网络传输、保存到数据库。当你需要把一个对象从一台机器发送到另一台机器,或者把对象持久化保存时,就需要序列化。
Java 原生提供了 Serializable 接口实现序列化,但这种方式存在很多问题(安全性差、无法跨语言、版本兼容困难),实际项目中更多使用 JSON、Protocol Buffers 等替代方案。理解序列化的原理和不同方案的优缺点,是选择合适方案的基础。
基础概念
什么是序列化
简单来说,序列化就是把对象变成可以存储或传输的格式。比如你有一个 User 对象,包含 name 和 age 两个字段,序列化后它可能变成一段 JSON 文本 {“name”:“Alice”,“age”:25},或者一段二进制数据。反序列化就是反过来,把这段数据还原成 User 对象。
为什么需要序列化
- 网络传输:微服务之间传递对象,需要把对象序列化后通过网络发送
- 持久化存储:把对象保存到文件或数据库,下次启动时恢复
- 缓存:把对象存入 Redis 等缓存系统,需要序列化后存储
- 跨语言通信:不同语言编写的系统之间交换数据
序列化方案的选择标准
选择序列化方案时需要考虑:是否需要跨语言(Java 序列化只有 Java 能用)、序列化后的数据大小(影响网络传输和存储成本)、序列化和反序列化的速度、是否人类可读(JSON 可读,二进制格式不可读)、Schema 演化能力(字段增删时是否兼容)。
快速上手
Java 原生序列化
最简单的序列化方式,只需实现 Serializable 接口:
import java.io.*;
// 实现 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;
}
// getter...
public String getName() { return name; }
public int getAge() { return age; }
public String getPassword() { return password; }
}
序列化和反序列化操作:
import java.io.*;
// 序列化:对象 -> 字节流
User user = new User("Alice", 25, "secret123");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"))) {
oos.writeObject(user); // 将对象写入文件
}
// 反序列化:字节流 -> 对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"))) {
User restored = (User) ois.readObject(); // 从文件读取对象
System.out.println(restored.getName()); // 输出: Alice
System.out.println(restored.getPassword()); // 输出: null(transient 字段不序列化)
}
详细用法
1. serialVersionUID 的作用
serialVersionUID 用于标识类的版本。反序列化时,JVM 会比较字节流中的 serialVersionUID 和当前类的 serialVersionUID,如果不一致就抛出 InvalidClassException:
public class User implements Serializable {
// 如果不手动指定,JVM 会根据类结构自动生成
// 但类结构变化(增删字段)后自动生成的值会改变,导致旧数据无法反序列化
// 手动指定后,只要版本号不变,增删字段也能兼容
private static final long serialVersionUID = 1L;
private String name;
private int age;
// 新增字段:旧数据反序列化时,新字段为默认值(null 或 0)
private String email;
}
建议始终手动指定 serialVersionUID,避免类结构变化导致的反序列化失败。
2. transient 关键字
transient 标记的字段不参与序列化,适合敏感数据或不需要持久化的数据:
public class Session implements Serializable {
private String sessionId;
private transient String password; // 密码不序列化
private transient Thread worker; // 线程对象无法序列化
private transient Socket connection; // 网络连接无法序列化
}
注意:transient 只能用于 Java 原生序列化。使用 JSON 序列化时,transient 不起作用,需要用 @JsonIgnore 等注解。
3. 自定义序列化逻辑
通过实现 writeObject 和 readObject 方法,可以自定义序列化和反序列化的行为:
import java.io.*;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient String encryptedPassword;
// 自定义序列化逻辑
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // 先执行默认序列化
// 可以在这里添加额外的序列化逻辑
}
// 自定义反序列化逻辑
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // 先执行默认反序列化
// 可以在这里恢复 transient 字段的值
// 例如:从其他来源重新获取密码
}
}
4. 使用 Jackson 进行 JSON 序列化
Jackson 是 Java 生态中最流行的 JSON 序列化库,可读性好、跨语言、使用广泛:
<!-- Maven 依赖 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.0</version>
</dependency>
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
public class User {
private String name;
private int age;
@JsonIgnore // JSON 序列化时忽略此字段
private String password;
@JsonProperty("user_email") // JSON 中的字段名
private String email;
// Jackson 需要无参构造函数
public User() {}
public User(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
// getter 和 setter...
}
// 序列化
ObjectMapper mapper = new ObjectMapper();
User user = new User("Alice", 25, "alice@example.com");
String json = mapper.writeValueAsString(user);
// {"name":"Alice","age":25,"user_email":"alice@example.com"}
// 反序列化
User restored = mapper.readValue(json, User.class);
5. Jackson 常用配置
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.text.SimpleDateFormat;
ObjectMapper mapper = new ObjectMapper();
// 格式化输出(开发调试时使用,生产环境关闭以减少数据量)
mapper.enable(SerializationFeature.INDENT_OUTPUT);
// 日期格式
mapper.registerModule(new JavaTimeModule()); // 支持 Java 8 日期类型
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // 日期输出为字符串而非时间戳
// 忽略未知字段(反序列化时 JSON 中有 Java 类没有的字段,不报错)
mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 空对象不报错
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
6. 使用 Gson 进行 JSON 序列化
Gson 是 Google 提供的 JSON 库,比 Jackson 更轻量:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
Gson gson = new GsonBuilder()
.setPrettyPrinting() // 格式化输出
.serializeNulls() // 序列化 null 值(默认忽略)
.setDateFormat("yyyy-MM-dd") // 日期格式
.create();
// 序列化
User user = new User("Alice", 25);
String json = gson.toJson(user);
// 反序列化
User restored = gson.fromJson(json, User.class);
7. 使用 Protocol Buffers
Protocol Buffers(Protobuf)是 Google 开发的高效二进制序列化格式,数据量小、速度快、支持跨语言:
// user.proto
syntax = "proto3";
message User {
string name = 1;
int32 age = 2;
string email = 3;
}
编译 proto 文件生成 Java 类后使用:
// 构建对象
UserProto.User user = UserProto.User.newBuilder()
.setName("Alice")
.setAge(25)
.setEmail("alice@example.com")
.build();
// 序列化为字节数组
byte[] bytes = user.toByteArray();
// 反序列化
UserProto.User restored = UserProto.User.parseFrom(bytes);
常见场景
场景一:API 响应序列化
Spring Boot 中 Controller 返回的对象自动序列化为 JSON:
import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(JsonInclude.Include.NON_NULL) // null 字段不输出
public class ApiResponse<T> {
private int code;
private String message;
private T data;
public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> response = new ApiResponse<>();
response.code = 200;
response.message = "success";
response.data = data;
return response;
}
}
场景二:Redis 缓存序列化
存入 Redis 的对象需要序列化,通常使用 JSON 格式以便跨语言读取:
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.core.StringRedisTemplate;
@Service
public class CacheService {
private final StringRedisTemplate redisTemplate;
private final ObjectMapper objectMapper;
// 存储对象到 Redis
public void put(String key, Object value) {
try {
String json = objectMapper.writeValueAsString(value);
redisTemplate.opsForValue().set(key, json);
} catch (Exception e) {
throw new RuntimeException("序列化失败", e);
}
}
// 从 Redis 读取对象
public <T> T get(String key, Class<T> type) {
String json = redisTemplate.opsForValue().get(key);
if (json == null) return null;
try {
return objectMapper.readValue(json, type);
} catch (Exception e) {
throw new RuntimeException("反序列化失败", e);
}
}
}
注意事项与常见错误
Java 原生序列化的安全风险
Java 原生反序列化存在严重的安全漏洞。攻击者可以构造恶意的序列化数据,在反序列化时执行任意代码。永远不要反序列化不可信来源的数据。如果必须使用,可以使用 JEP 290 过滤机制限制可反序列化的类。
循环引用导致无限递归
对象之间存在双向引用时,JSON 序列化会陷入无限递归:
public class User {
private List<Order> orders; // 用户有多个订单
}
public class Order {
private User user; // 订单属于某个用户(循环引用)
}
// 解决方案一:使用 @JsonIgnore 忽略反向引用
public class Order {
@JsonIgnore
private User user;
}
// 解决方案二:使用 @JsonManagedReference 和 @JsonBackReference
public class User {
@JsonManagedReference
private List<Order> orders;
}
public class Order {
@JsonBackReference
private User user;
}
日期格式问题
不同序列化方式对日期的处理不同。Java 原生序列化保存的是时间戳,JSON 序列化默认输出时间戳数字。建议统一使用 ISO 8601 格式(如 2024-01-15T10:30:00Z)。
枚举类型的序列化
默认情况下枚举按名称序列化。如果按序号序列化,新增枚举值可能导致反序列化错位:
// 错误:按序号序列化,新增值会打乱顺序
public enum Status {
ACTIVE, // 0
INACTIVE, // 1
// 新增 PENDING 后,原来存为 1 的 INACTIVE 会变成 PENDING
PENDING // 2
}
// 正确:始终按名称序列化
@JsonFormat(shape = JsonFormat.Shape.STRING)
public enum Status {
ACTIVE, INACTIVE, PENDING
}
进阶用法
Kryo 高性能序列化
Kryo 是一个高性能的 Java 序列化框架,速度比 Java 原生序列化快 10 倍以上,序列化后的数据也更小:
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Output;
import com.esotericsoftware.kryo.io.Input;
Kryo kryo = new Kryo();
kryo.register(User.class); // 注册类,提高性能
// 序列化
Output output = new Output(new ByteArrayOutputStream());
kryo.writeObject(output, user);
output.close();
// 反序列化
Input input = new Input(new ByteArrayInputStream(output.getBuffer()));
User restored = kryo.readObject(input, User.class);
Avro Schema 演化
Avro 支持 Schema 演化,可以在不破坏兼容性的情况下增删字段。适合需要长期存储和版本迭代的数据格式:
{
"type": "record",
"name": "User",
"fields": [
{ "name": "name", "type": "string" },
{ "name": "age", "type": ["null", "int"], "default": null }
]
}
新增字段时提供默认值,旧数据反序列化时新字段使用默认值,不会报错。
Serializable 接口
基本写法:实现可序列化
class <类名> implements Serializable {}
// 标记类为可序列化
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
}
基本写法:定义 serialVersionUID
private static final long serialVersionUID = <值>L;
// 显式声明版本号保证兼容
private static final long serialVersionUID = 42L;
对象流序列化
基本写法:写入对象
new ObjectOutputStream(<输出流>).writeObject(<对象>);
// 序列化对象到文件
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("user.dat"))) {
oos.writeObject(user);
}
基本写法:读取对象
new ObjectInputStream(<输入流>).readObject();
// 从文件反序列化对象
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("user.dat"))) {
User u = (User) ois.readObject();
}
关键字 transient
基本写法:排除字段
transient <类型> <字段名>;
// 标记字段不参与序列化
private transient String password;
自定义序列化
基本写法:重写 writeObject
private void writeObject(ObjectOutputStream out) throws IOException {}
// 自定义写入逻辑
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeUTF(encrypt(password));
}
基本写法:重写 readObject
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {}
// 自定义读取逻辑
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.password = decrypt(in.readUTF());
}
基本写法:writeReplace 替换对象
private Object writeReplace() { return <新对象>; }
// 序列化时替换为另一个对象
private Object writeReplace() {
return new UserProxy(name);
}
基本写法:readResolve 单例恢复
private Object readResolve() { return <实例>; }
// 反序列化时返回单例实例
private Object readResolve() {
return INSTANCE;
}
Externalizable 接口
基本写法:实现 Externalizable
class <类名> implements Externalizable { public void writeExternal(ObjectOutput o) {} public void readExternal(ObjectInput i) {} }
// 完全自定义序列化
public class User implements Externalizable {
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
}
public void readExternal(ObjectInput in) throws IOException {
this.name = in.readUTF();
}
}
序列化过滤(Java 9+)
基本写法:设置输入过滤器
ObjectInputFilter.Config.createFilter("<规则>")
// 反序列化白名单过滤
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.*;java.lang.*;!*");
ois.setObjectInputFilter(filter);
基本写法:全局过滤器
ObjectInputFilter.Config.setSerialFilter(<过滤器>);
// 设置全局反序列化过滤器
ObjectInputFilter.Config.setSerialFilter(info -> {
if (info.serialClass() == User.class) return ObjectInputFilter.Status.ALLOWED;
return ObjectInputFilter.Status.REJECTED;
});
JSON 序列化(Jackson)
基本写法:Jackson 写 JSON
new ObjectMapper().writeValueAsString(<对象>);
// 将对象序列化为 JSON 字符串
String json = new ObjectMapper().writeValueAsString(user);
基本写法:Jackson 读 JSON
new ObjectMapper().readValue(<json>, <类>.class);
// 将 JSON 字符串反序列化为对象
User u = new ObjectMapper().readValue(json, User.class);
基本写法:Jackson 写文件
new ObjectMapper().writeValue(<文件>, <对象>);
// 将对象序列化到 JSON 文件
new ObjectMapper().writeValue(new File("user.json"), user);
基本写法:忽略字段
@JsonIgnore
// 标记字段不参与 JSON 序列化
@JsonIgnore
private String password;
基本写法:指定字段名
@JsonProperty("<名称>")
// 自定义 JSON 字段名
@JsonProperty("user_name")
private String userName;
JSON 序列化(Gson)
基本写法:Gson 写 JSON
new Gson().toJson(<对象>);
// 使用 Gson 序列化为 JSON
String json = new Gson().toJson(user);
基本写法:Gson 读 JSON
new Gson().fromJson(<json>, <类>.class);
// 使用 Gson 反序列化
User u = new Gson().fromJson(json, User.class);
ProtoBuf 二进制序列化
基本写法:ProtoBuf 写入
<消息类>.writeTo(<输出流>);
// ProtoBuf 消息写入字节流
UserProto.User u = UserProto.User.newBuilder().setName("Alice").build();
u.writeTo(new FileOutputStream("u.bin"));
基本写法:ProtoBuf 读取
<消息类>.parseFrom(<输入流>);
// 从字节流解析 ProtoBuf 消息
UserProto.User u = UserProto.User.parseFrom(new FileInputStream("u.bin"));