1. 简介
本文将快速介绍 Java 中的 java.io.Externalizable
接口。该接口的核心目标是实现自定义序列化和反序列化。
在继续之前,建议先阅读 Java 序列化 相关文章。下一章将展示如何使用该接口序列化 Java 对象。
之后,我们将重点对比它与 java.io.Serializable
接口的关键差异。
2. Externalizable 接口详解
Externalizable
继承自 java.io.Serializable
标记接口。任何实现 Externalizable
接口的类都必须重写 writeExternal()
和 readExternal()
方法,从而改变 JVM 的默认序列化行为。
2.1 序列化实现
看这个简单示例:
public class Country implements Externalizable {
private static final long serialVersionUID = 1L;
private String name;
private int code;
// getters, setters
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(code);
}
@Override
public void readExternal(ObjectInput in)
throws IOException, ClassNotFoundException {
this.name = in.readUTF();
this.code = in.readInt();
}
}
这里定义了 Country
类实现 Externalizable
接口,并重写了两个核心方法:
✅ 在 writeExternal()
中:将对象属性写入 ObjectOutput
流
✅ 在 readExternal()
中:从 ObjectInput
流按相同顺序读取属性
⚠️ 最佳实践:手动添加 serialVersionUID
。若缺失,JVM 会自动生成一个编译器依赖的 ID,可能导致 InvalidClassException
异常。
测试上述实现:
@Test
public void whenSerializing_thenUseExternalizable()
throws IOException, ClassNotFoundException {
Country c = new Country();
c.setCode(374);
c.setName("Armenia");
FileOutputStream fileOutputStream
= new FileOutputStream(OUTPUT_FILE);
ObjectOutputStream objectOutputStream
= new ObjectOutputStream(fileOutputStream);
c.writeExternal(objectOutputStream);
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();
FileInputStream fileInputStream
= new FileInputStream(OUTPUT_FILE);
ObjectInputStream objectInputStream
= new ObjectInputStream(fileInputStream);
Country c2 = new Country();
c2.readExternal(objectInputStream);
objectInputStream.close();
fileInputStream.close();
assertTrue(c2.getCode() == c.getCode());
assertTrue(c2.getName().equals(c.getName()));
}
测试流程:
- 创建
Country
对象并写入文件 - 从文件反序列化对象
- 验证数据一致性
反序列化后的对象输出:
Country{name='Armenia', code=374}
2.2 继承场景
当类继承 Serializable
接口时,JVM 会自动收集子类所有字段进行序列化。**Externalizable
同样支持继承,但需要为继承链中的每个子类实现读/写方法**。
看下述 Region
类(继承自前文的 Country
类):
public class Region extends Country implements Externalizable {
private static final long serialVersionUID = 1L;
private String climate;
private Double population;
// getters, setters
@Override
public void writeExternal(ObjectOutput out) throws IOException {
super.writeExternal(out);
out.writeUTF(climate);
}
@Override
public void readExternal(ObjectInput in)
throws IOException, ClassNotFoundException {
super.readExternal(in);
this.climate = in.readUTF();
}
}
关键点:
✅ 通过 super.writeExternal(out)
和 super.readExternal(in)
处理父类字段
❌ 未序列化 population
字段
测试数据:
Region r = new Region();
r.setCode(374);
r.setName("Armenia");
r.setClimate("Mediterranean");
r.setPopulation(120.000);
反序列化结果:
Region{
country='Country{
name='Armenia',
code=374}'
climate='Mediterranean',
population=null
}
⚠️ 踩坑提醒:未序列化的 population
字段在反序列化后为 null
。
3. Externalizable vs Serializable 对比
维度 | Serializable | Externalizable |
---|---|---|
序列化责任 | JVM 全权负责 | 程序员完全控制 |
适用场景 | 需要序列化整个对象时 | 需要精细控制序列化过程时 |
性能 | 使用反射和元数据,性能较低 | 直接控制序列化逻辑,性能更高 |
读取顺序 | 无严格要求 | 必须严格按写入顺序读取,否则抛异常 |
自定义序列化 | 通过 transient 关键字跳过字段 |
完全自定义序列化逻辑 |
关键差异说明:
序列化责任
Serializable
:JVM 自动处理Externalizable
:程序员需手动实现序列化/反序列化逻辑
读取顺序
// 错误示例:顺序颠倒会导致 java.io.EOFException @Override public void readExternal(ObjectInput in) throws IOException { this.code = in.readInt(); // 先读 code this.name = in.readUTF(); // 后读 name(与写入顺序不符) }
自定义序列化
Serializable
:用transient
跳过字段,但字段仍会以默认值存储Externalizable
:完全控制哪些字段序列化及如何存储
4. 总结
本文通过示例展示了 Externalizable
接口的核心特性:
- ✅ 完全控制序列化过程
- ✅ 相比
Serializable
性能更优 - ⚠️ 需严格处理字段读写顺序
- ⚠️ 继承时需逐层实现序列化方法
当需要精细控制序列化逻辑或追求高性能时,Externalizable
是比 Serializable
更优的选择。
完整源代码见 GitHub 仓库。