1. 引言
在 Java 中,最常用的对象序列化方式是实现接口 Serializable。它很简单:实现接口之后,就可以通过 ObjectOutputStream/ObjectInputStream 来写入/读取对象。但有时这还不够:
- 需要完全控制哪些字段被序列化,以及如何序列化。
- 需要在类的不同版本之间保持兼容性。
- 希望减小序列化文件的大小或提升序列化/反序列化速度。
针对这些场景,Java 提供了接口 Externalizable——一种更“手动”、更灵活的序列化方式。
要点速览:
- Serializable — 自动序列化:由 Java 自行决定写什么、如何写。
- Externalizable — 手动序列化:由你明确指定要保存/恢复什么以及如何处理。
2. 契约 Externalizable:实现 writeExternal 和 readExternal
要使用 Externalizable,需要:
- 实现接口 java.io.Externalizable。
- 必须实现两个方法:
- 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 时,你只写入真正需要的数据。
- 保障向后兼容性。 如果类结构发生变化,可以手动实现读取旧版与新版数据的逻辑。
- 序列化非常规对象。 例如,存在无法用标准方式序列化的字段(如 transient、volatile,或复杂结构)。
何时不应使用?
- 如果你并不需要完全控制——使用 Serializable,更简单也更省心。
- 如果无法保证在类变更时长期维护数据格式的兼容性。
4. Externalizable 相对于 Serializable 的优缺点
优点:
- 对序列化拥有完全控制权。 由你决定如何写入/读取。
- 更紧凑。 没有多余的元数据——只有你的数据。
- 速度更快。 数据更少,读写更快。
- 灵活。 可实现多版本格式、加入压缩、加密等。
缺点:
- 需要手动实现——容易出错。 如果读写顺序不一致,序列化会“崩掉”(抛错或数据不正确)。
- 没有自动支持 transient、serialVersionUID。一切都要自行设计并实现。
- 维护更复杂。 当类结构变化时,需要同步更新序列化方法。
- 必须提供 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)
}
注意:如果更改了字段写入/读取的顺序,或遗漏了某个字段,数据将会不正确!writeExternal 和 readExternal 必须在操作顺序上严格一致。
示例:只序列化部分字段
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. 实践:比较序列化文件大小
我们来比较,通过 Serializable 与 Externalizable 序列化后的文件有多“重”。
使用 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:读写字段顺序不一致。
方法 writeExternal 与 readExternal 必须使用相同的顺序。如果写入时先保存 name,而读取时先尝试读取 age,数据将被破坏。
错误 3:序列化时遗漏字段。
如果在 writeExternal 中忘记写入某个字段,反序列化后它将是 null(引用类型)或 0(数值类型)。
错误 4:错误使用 transient 或 serialVersionUID。
不同于 Serializable,对于 Externalizable 这些机制不会自动生效——你必须自行决定保存哪些字段、忽略哪些字段。
错误 5:修改类结构却没有更新方法。
如果添加或删除了字段,却没有相应地更新 writeExternal 和 readExternal,旧的已保存数据可能无法被正确加载。
GO TO FULL VERSION