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 接口实现,它将对象的属性和结构编码为字节序列,以便存储或传输。

关键特性

  1. 递归性:序列化一个对象时,会自动处理其引用的对象(例如,对象中的成员变量)。
  2. 版本兼容性:对象的类结构修改后,反序列化可能失败,需通过 serialVersionUID 管理版本。
  3. 安全性:反序列化可能引入恶意代码,需谨慎处理来源不明的数据。

如何实现 Java 序列化?

第一步:标记可序列化的类

要使一个类支持序列化,必须实现 Serializable 接口。这是一个标记接口,无需实现任何方法。

public class User implements Serializable {  
    private String name;  
    private int age;  
    // 构造方法、getter/setter 省略  
}  

第二步:使用流进行序列化与反序列化

Java 提供了 ObjectOutputStreamObjectInputStream 用于序列化和反序列化操作。

示例:将对象写入文件

// 序列化操作  
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:序列化后文件过大

当对象包含大量数据或复杂嵌套时,生成的字节流可能占用过多空间。可以通过以下方式优化:

  1. 去除不必要的字段:使用 transient 关键字排除无关属性。
  2. 自定义序列化:通过 writeObjectreadObject 方法手动控制序列化过程。

示例:自定义序列化

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:类版本不一致导致反序列化失败

当类的结构(如字段名、类型)发生变化时,旧版本的序列化数据可能无法读取。可通过以下方法解决:

  1. 显式声明 serialVersionUID

    private static final long serialVersionUID = 1L;  
    

    若修改类后需保持兼容,可保留旧的 serialVersionUID

  2. 使用兼容性策略:例如,通过 readObject 方法动态处理新增或删除的字段。


进阶话题:序列化的高级用法

自定义序列化规则

默认序列化可能无法满足复杂需求,例如:

  • 需要加密敏感字段;
  • 需要压缩序列化后的数据。

通过重写 writeObjectreadObject 方法,可以完全控制序列化过程。例如,加密密码字段:

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<>();  
// 添加用户并序列化  

最佳实践与安全提示

实践建议

  1. 显式声明 serialVersionUID:避免因类修改导致的兼容性问题。
  2. 谨慎使用 transient:确保非序列化字段的逻辑不会破坏对象完整性。
  3. 测试序列化过程:在修改类结构后,验证反序列化的正确性。

安全风险

反序列化任意数据可能导致 反序列化漏洞(如 CVE-2015-4852)。解决方案包括:

  • 限制反序列化来源:仅处理可信数据源。
  • 使用第三方库:例如 Kryo 或 Protobuf 替代默认序列化,减少风险。

结论

Java 序列化是一个强大但需谨慎使用的工具。通过本文的讲解,读者应能理解其核心机制、实现步骤及常见问题的解决方案。无论是开发分布式系统、缓存持久化,还是处理网络通信,掌握序列化技术都能显著提升代码的灵活性和健壮性。

在实践中,建议结合具体场景选择序列化策略(如 JSON、XML 或二进制协议),并始终关注安全性与兼容性。随着对 Java 序列化的深入理解,开发者将能够更高效地应对复杂的数据交互需求。


(全文约 1600 字)

最新发布