Java 序列化:Serializable 与 Externalizable
序列化是 Java 开发中不可或缺的基础机制,它让对象能够脱离 JVM 运行环境持久保存,或在网络中高效传输。然而,很多开发者对序列化的理解仅限于给类加上 Serializable 接口,对序列化的底层机制和控制手段知之甚少。本文将深入剖析 Java 序列化的两种核心方式,帮你彻底掌握这门技术。
什么是序列化?
序列化是将 Java 对象转换为字节流的过程,反序列化则是将字节流恢复为对象的过程。这一机制解决了两个核心问题:对象持久化(将对象保存到磁盘或数据库)和对象传输(在网络中将对象从一个 JVM 发送到另一个 JVM)。
Java 提供了两种序列化方式:自动序列化和手动序列化。前者通过 Serializable 接口实现,后者通过 Externalizable 接口实现。理解两者的差异,是写出高效、可靠的序列化代码的前提。
Serializable 接口:自动序列化
基本用法
Serializable 是一个标记接口(Marker Interface),它不包含任何方法声明。实现该接口的类,其对象将被 Java 序列化机制自动处理。
import java.io.*;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private transient String password; // 被 transient 修饰的字段不参与序列化
// 构造方法、getter、setter
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 + "'}";
}
}
执行序列化与反序列化:
public class SerializableDemo {
public static void main(String[] args) {
User user = new User("张三", 25, "secret123");
// 序列化:对象 → 字节流
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("user.ser"))) {
oos.writeObject(user);
System.out.println("序列化完成");
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化:字节流 → 对象
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("user.ser"))) {
User restoredUser = (User) ois.readObject();
System.out.println("反序列化结果: " + restoredUser);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出:
序列化完成
反序列化结果: User{name='张三', age=25, password='null'}
注意 password 字段因为被 transient 修饰,反序列化后值为 null。这是控制序列化行为的最简单方式。
serialVersionUID 的重要性
每个可序列化的类都有一个版本标识符 serialVersionUID。反序列化时,JVM 会比对字节流中的 UID 与类中定义的 UID 是否一致,不一致则抛出 InvalidClassException。
// 强烈建议显式声明 serialVersionUID
private static final long serialVersionUID = 20240701L;
如果不显式声明,JVM 会根据类的成员结构自动生成一个 UID。这意味着:当类结构发生变化(如添加字段),旧版本序列化出的字节流将无法被新版本的类正确反序列化。显式声明 UID 可以让类在结构变化后仍然兼容旧数据。
Externalizable 接口:完全控制序列化
Externalizable 接口继承自 Serializable,但它要求开发者完全自己控制序列化和反序列化的过程。实现该接口的类必须实现两个方法:writeExternal() 和 readExternal()。
基本用法
import java.io.*;
public class Product implements Externalizable {
private String name;
private double price;
private int stock;
// Externalizable 要求必须有一个无参构造方法
public Product() {}
public Product(String name, double price, int stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// 完全自定义序列化逻辑
out.writeUTF(name); // 只序列化 name
out.writeDouble(price); // 只序列化 price
// 注意:stock 字段被跳过,不参与序列化
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// 必须严格按照 writeExternal 的顺序读取
this.name = in.readUTF();
this.price = in.readDouble();
// stock 保持默认值 0
}
@Override
public String toString() {
return "Product{name='" + name + "', price=" + price + ", stock=" + stock + "}";
}
}
执行序列化与反序列化:
public class ExternalizableDemo {
public static void main(String[] args) {
Product product = new Product("笔记本电脑", 6999.00, 100);
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("product.ser"))) {
oos.writeObject(product);
System.out.println("序列化完成");
} catch (IOException e) {
e.printStackTrace();
}
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("product.ser"))) {
Product restoredProduct = (Product) ois.readObject();
System.out.println("反序列化结果: " + restoredProduct);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出:
序列化完成
反序列化结果: Product{name='笔记本电脑', price=6999.0, stock=0}
stock 字段在 writeExternal() 中没有被写入,因此反序列化后为默认值 0。这展示了 Externalizable 提供的细粒度控制能力。
关键细节
使用 Externalizable 时必须注意以下几点:
无参构造方法是必需的。反序列化时,JVM 会先调用无参构造方法创建对象,然后再调用 readExternal() 填充数据。如果没有无参构造方法,将抛出 InvalidClassException。
读写顺序必须严格一致。readExternal() 中读取字段的顺序、数据类型必须与 writeExternal() 中写入的顺序完全匹配,否则数据会错乱。
核心差异对比
| 特性 | Serializable | Externalizable |
|---|---|---|
| 接口类型 | 标记接口 | 普通接口(需实现两个方法) |
| 控制粒度 | 粗粒度(只能通过 transient 排除字段) |
细粒度(完全自定义序列化逻辑) |
| 性能 | 一般(反射机制,速度较慢) | 优秀(直接调用方法,无反射开销) |
| 安全性 | 低(自动序列化可能泄露敏感数据) | 高(可精确控制哪些数据被序列化) |
| 版本兼容性 | 依赖 serialVersionUID |
同左,但需自行处理字段加减 |
| 实现复杂度 | 低(只需实现接口) | 高(需自行编写序列化逻辑) |
实战场景与选型建议
场景一:简单数据传输
如果对象结构简单,只需要持久化或传输基本数据,且对性能要求不高,选择 Serializable。它实现简单、维护成本低,适合大多数场景。
场景二:高性能要求
如果序列化是系统性能瓶颈(如高频交易系统、大数据处理),Externalizable 是更好的选择。它避免了反射开销,可以写出针对性的高效序列化代码。
场景三:敏感数据处理
如果对象包含密码、密钥等敏感字段,Externalizable 允许你精确控制只序列化必要字段,避免敏感数据泄露。
场景四:复杂版本演进
如果类结构会频繁变化,且需要保持与旧版本数据的兼容性,Externalizable 让你可以在读写时进行版本判断和处理,实现平滑升级。
最佳实践总结
始终显式声明 serialVersionUID。即使当前不需要考虑版本兼容,显式声明也是一个好习惯,它让类的演化更加可控。
使用 transient 保护敏感数据。密码、令牌、私钥等字段应该标记为 transient,确保它们不会被意外序列化。
优先选择 Serializable。除非有明确的性能或安全需求,否则 Serializable 是更实用的选择。过度优化是万恶之源。
在 Externalizable 中验证数据。readExternal() 是反序列化入口,可以在这里添加数据校验逻辑,增强系统健壮性。
考虑使用替代方案。JSON(如 Jackson、Gson)、Protocol Buffers、Kyro 等现代序列化框架在性能、跨语言支持、可读性方面往往优于原生 Java 序列化。

暂无评论,快来抢沙发吧!