Java 序列化(一文讲透)
💡一则或许对你有用的小广告
欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...
,点击查看项目介绍 ;- 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;
截止目前, 星球 内专栏累计输出 82w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 2900+ 小伙伴加入学习 ,欢迎点击围观
前言
在软件开发中,对象的持久化(即保存到磁盘或网络传输)是一个常见需求。想象一下,当你需要将一个复杂的对象保存到文件中,或者通过网络传输到另一台计算机时,如何确保对象的数据结构和状态能够完整保留?这就需要借助 Java 序列化这一机制。
Java 序列化是 Java 提供的将对象转换为字节流,并在需要时重新还原为对象的技术。它广泛应用于 RPC(远程过程调用)、分布式系统、缓存持久化等场景。然而,对于初学者而言,这一概念可能显得抽象。本文将通过循序渐进的方式,结合实例和比喻,帮助读者理解 Java 序列化的核心原理、实现方法及常见问题。
什么是 Java 序列化?
核心概念:对象与字节流
可以将序列化想象为一种“快递服务”:
- 对象 是包裹,包含数据(如商品、收件人信息);
- 序列化 是将包裹拆解成可运输的零件(字节流);
- 反序列化 是在目的地重新组装成原包裹。
Java 序列化的核心是通过 Serializable
接口实现,它将对象的属性和结构编码为字节序列,以便存储或传输。
关键特性
- 递归性:序列化一个对象时,会自动处理其引用的对象(例如,对象中的成员变量)。
- 版本兼容性:对象的类结构修改后,反序列化可能失败,需通过
serialVersionUID
管理版本。 - 安全性:反序列化可能引入恶意代码,需谨慎处理来源不明的数据。
如何实现 Java 序列化?
第一步:标记可序列化的类
要使一个类支持序列化,必须实现 Serializable
接口。这是一个标记接口,无需实现任何方法。
public class User implements Serializable {
private String name;
private int age;
// 构造方法、getter/setter 省略
}
第二步:使用流进行序列化与反序列化
Java 提供了 ObjectOutputStream
和 ObjectInputStream
用于序列化和反序列化操作。
示例:将对象写入文件
// 序列化操作
User user = new User("Alice", 30);
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.data"))) {
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
}
示例:从文件读取对象
User deserializedUser = null;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.data"))) {
deserializedUser = (User) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
注意事项
- 瞬态字段:若某字段不需要序列化(如临时计算结果),可使用
transient
关键字标记。 - 静态字段:静态字段不会被序列化,因为它们属于类而非实例。
常见问题与解决方案
问题 1:序列化后文件过大
当对象包含大量数据或复杂嵌套时,生成的字节流可能占用过多空间。可以通过以下方式优化:
- 去除不必要的字段:使用
transient
关键字排除无关属性。 - 自定义序列化:通过
writeObject
和readObject
方法手动控制序列化过程。
示例:自定义序列化
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // 先执行默认序列化
oos.writeUTF("额外数据"); // 添加自定义数据
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
String extra = ois.readUTF();
}
问题 2:类版本不一致导致反序列化失败
当类的结构(如字段名、类型)发生变化时,旧版本的序列化数据可能无法读取。可通过以下方法解决:
-
显式声明 serialVersionUID:
private static final long serialVersionUID = 1L;
若修改类后需保持兼容,可保留旧的
serialVersionUID
。 -
使用兼容性策略:例如,通过
readObject
方法动态处理新增或删除的字段。
进阶话题:序列化的高级用法
自定义序列化规则
默认序列化可能无法满足复杂需求,例如:
- 需要加密敏感字段;
- 需要压缩序列化后的数据。
通过重写 writeObject
和 readObject
方法,可以完全控制序列化过程。例如,加密密码字段:
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // 序列化普通字段
String encryptedPassword = encrypt(password);
oos.writeUTF(encryptedPassword);
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
String encrypted = ois.readUTF();
password = decrypt(encrypted);
}
序列化与集合类
集合类(如 ArrayList
)的序列化会递归处理其元素。例如,序列化一个 List<User>
时,会自动序列化所有 User
对象。
List<User> users = new ArrayList<>();
// 添加用户并序列化
最佳实践与安全提示
实践建议
- 显式声明 serialVersionUID:避免因类修改导致的兼容性问题。
- 谨慎使用 transient:确保非序列化字段的逻辑不会破坏对象完整性。
- 测试序列化过程:在修改类结构后,验证反序列化的正确性。
安全风险
反序列化任意数据可能导致 反序列化漏洞(如 CVE-2015-4852)。解决方案包括:
- 限制反序列化来源:仅处理可信数据源。
- 使用第三方库:例如 Kryo 或 Protobuf 替代默认序列化,减少风险。
结论
Java 序列化是一个强大但需谨慎使用的工具。通过本文的讲解,读者应能理解其核心机制、实现步骤及常见问题的解决方案。无论是开发分布式系统、缓存持久化,还是处理网络通信,掌握序列化技术都能显著提升代码的灵活性和健壮性。
在实践中,建议结合具体场景选择序列化策略(如 JSON、XML 或二进制协议),并始终关注安全性与兼容性。随着对 Java 序列化的深入理解,开发者将能够更高效地应对复杂的数据交互需求。
(全文约 1600 字)