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()));
}

测试流程:

  1. 创建 Country 对象并写入文件
  2. 从文件反序列化对象
  3. 验证数据一致性

反序列化后的对象输出:

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 关键字跳过字段 完全自定义序列化逻辑

关键差异说明:

  1. 序列化责任

    • Serializable:JVM 自动处理
    • Externalizable程序员需手动实现序列化/反序列化逻辑
  2. 读取顺序

    // 错误示例:顺序颠倒会导致 java.io.EOFException
    @Override
    public void readExternal(ObjectInput in) throws IOException {
        this.code = in.readInt();  // 先读 code
        this.name = in.readUTF();  // 后读 name(与写入顺序不符)
    }
    
  3. 自定义序列化

    • Serializable:用 transient 跳过字段,但字段仍会以默认值存储
    • Externalizable完全控制哪些字段序列化及如何存储

4. 总结

本文通过示例展示了 Externalizable 接口的核心特性:

  • ✅ 完全控制序列化过程
  • ✅ 相比 Serializable 性能更优
  • ⚠️ 需严格处理字段读写顺序
  • ⚠️ 继承时需逐层实现序列化方法

当需要精细控制序列化逻辑或追求高性能时,Externalizable 是比 Serializable 更优的选择。

完整源代码见 GitHub 仓库


原始标题:Guide to the Externalizable Interface in Java