CodeGym /课程 /JAVA 25 SELF /Externalizable:序列化的精细化定制

Externalizable:序列化的精细化定制

JAVA 25 SELF
第 43 级 , 课程 2
可用

1. 引言

在 Java 中,最常用的对象序列化方式是实现接口 Serializable。它很简单:实现接口之后,就可以通过 ObjectOutputStream/ObjectInputStream 来写入/读取对象。但有时这还不够:

  • 需要完全控制哪些字段被序列化,以及如何序列化。
  • 需要在类的不同版本之间保持兼容性。
  • 希望减小序列化文件的大小或提升序列化/反序列化速度。

针对这些场景,Java 提供了接口 Externalizable——一种更“手动”、更灵活的序列化方式。

要点速览:

  • Serializable — 自动序列化:由 Java 自行决定写什么、如何写。
  • Externalizable — 手动序列化:由你明确指定要保存/恢复什么以及如何处理。

2. 契约 Externalizable:实现 writeExternalreadExternal

要使用 Externalizable,需要:

  1. 实现接口 java.io.Externalizable
  2. 必须实现两个方法:
    • void writeExternal(ObjectOutput out) throws IOException
    • void readExternal(ObjectInput in) throws IOException, ClassNotFoundException

示例:

import java.io.*;

public class User implements Externalizable {
    private String name;
    private int age;

    // 必须提供 public 无参构造器!
    public User() {}

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = in.readUTF();
        age = in.readInt();
    }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

重要:开发者可以自行决定哪些字段被序列化以及顺序。但有一个硬性要求:类必须具有一个 public 无参构造器。否则,在反序列化时程序会抛出 InvalidClassException

3. 何时使用 Externalizable

在以下情况下使用 Externalizable

  • 需要对数据格式进行完全控制。 例如,只序列化部分字段,或以特定顺序/格式进行序列化。
  • 优化性能与文件大小。 标准序列化会附加开销信息(元数据、类名、类型等)。使用 Externalizable 时,你只写入真正需要的数据。
  • 保障向后兼容性。 如果类结构发生变化,可以手动实现读取旧版与新版数据的逻辑。
  • 序列化非常规对象。 例如,存在无法用标准方式序列化的字段(如 transientvolatile,或复杂结构)。

何时不应使用?

  • 如果你并不需要完全控制——使用 Serializable,更简单也更省心。
  • 如果无法保证在类变更时长期维护数据格式的兼容性。

4. Externalizable 相对于 Serializable 的优缺点

优点:

  • 对序列化拥有完全控制权。 由你决定如何写入/读取。
  • 更紧凑。 没有多余的元数据——只有你的数据。
  • 速度更快。 数据更少,读写更快。
  • 灵活。 可实现多版本格式、加入压缩、加密等。

缺点:

  • 需要手动实现——容易出错。 如果读写顺序不一致,序列化会“崩掉”(抛错或数据不正确)。
  • 没有自动支持 transientserialVersionUID。一切都要自行设计并实现。
  • 维护更复杂。 当类结构变化时,需要同步更新序列化方法。
  • 必须提供 public 无参构造器。
  • “魔法”更少——责任更大。

5. 示例:简单对象的序列化与反序列化

对象的序列化

User user = new User("Alice", 30);

try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
    out.writeObject(user);
}

对象的反序列化

try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
    User loaded = (User) in.readObject();
    System.out.println(loaded); // Alice (30)
}

注意:如果更改了字段写入/读取的顺序,或遗漏了某个字段,数据将会不正确!writeExternalreadExternal 必须在操作顺序上严格一致。

示例:只序列化部分字段

public class SecretUser implements Externalizable {
    private String login;
    private transient String password; // 对 Externalizable 而言,transient 不起作用

    public SecretUser() {}

    public SecretUser(String login, String password) {
        this.login = login;
        this.password = password;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(login);
        // 不序列化密码!
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        login = in.readUTF();
        password = null; // 不恢复密码
    }
}

6. 实践:比较序列化文件大小

我们来比较,通过 SerializableExternalizable 序列化后的文件有多“重”。

使用 Serializable 的类

public class UserSerializable implements Serializable {
    private String name;
    private int age;

    public UserSerializable(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

使用 Externalizable 的类

public class UserExternalizable implements Externalizable {
    private String name;
    private int age;

    public UserExternalizable() {}

    public UserExternalizable(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = in.readUTF();
        age = in.readInt();
    }
}

用于对比的代码

import java.io.*;

public class CompareSerialization {
    public static void main(String[] args) throws Exception {
        UserSerializable s = new UserSerializable("Bob", 25);
        UserExternalizable e = new UserExternalizable("Bob", 25);

        // Serializable
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin"))) {
            out.writeObject(s);
        }

        // Externalizable
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ext.bin"))) {
            out.writeObject(e);
        }

        System.out.println("Serializable file size: " + new File("ser.bin").length());
        System.out.println("Externalizable file size: " + new File("ext.bin").length());
    }
}

结果:
文件 ser.bin(Serializable)通常更大——包含 Java 的开销信息。文件 ext.bin(Externalizable)只有你的数据,通常更小。

7. 使用 Externalizable 时的常见错误

错误 1:缺少 public 无参构造器。
实现 Externalizable 的类必须有一个 public 无参构造器。否则反序列化会抛出 InvalidClassException

错误 2:读写字段顺序不一致。
方法 writeExternalreadExternal 必须使用相同的顺序。如果写入时先保存 name,而读取时先尝试读取 age,数据将被破坏。

错误 3:序列化时遗漏字段。
如果在 writeExternal 中忘记写入某个字段,反序列化后它将是 null(引用类型)或 0(数值类型)。

错误 4:错误使用 transientserialVersionUID
不同于 Serializable,对于 Externalizable 这些机制不会自动生效——你必须自行决定保存哪些字段、忽略哪些字段。

错误 5:修改类结构却没有更新方法。
如果添加或删除了字段,却没有相应地更新 writeExternalreadExternal,旧的已保存数据可能无法被正确加载。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION