1. writeReplace 与 readResolve:理论
有时标准的序列化机制并不够用。设想这样一种情况:你有一个单例(在整个程序中只能有一个实例的类),你希望反序列化后它依然是那个唯一实例(而不是出现一个新的克隆)。或者,你希望序列化的不是对象本身,而是它的“轻量”版本(代理),以隐藏实现细节或节省空间。
在 Java 中为此提供了专用方法:writeReplace 和 readResolve。它们的职责是——将即将被序列化或刚被反序列化的对象替换为另一个对象。
一个简单的类比:
这就好比你要给朋友寄包裹,但你把自己的玩具替身放进了箱子。等朋友拆开包裹时,手里的“玩具”却变成了真正的你!(现实里当然做不到,但在 Java 里——可以。)
writeReplace
方法 private Object writeReplace() 会在对象序列化之前被调用。它可以返回任意对象,该对象将实际被序列化来替代原始对象。如果未实现——就序列化对象本身。
签名:
private Object writeReplace() throws ObjectStreamException
readResolve
方法 private Object readResolve() 会在对象反序列化之后被调用。它允许将刚创建出来的对象替换为另一个对象(例如返回单例或缓存的实例)。
签名:
private Object readResolve() throws ObjectStreamException
重要:
这两个方法都必须是 private 且返回 Object。这是 Java 序列化规范的要求。如果将它们设为 public——序列化机制会直接忽略它们。
2. 在实践中使用 writeReplace 与 readResolve
单例与 readResolve
单例就是在整个程序中只能有一个实例的类。如果序列化这样的对象再恢复,若没有 readResolve,会出现一个新实例,从而破坏“唯一性”的约束。而使用 readResolve,就可以返回那个唯一对象,保持单例的语义。
import java.io.*;
public class MySingleton implements Serializable {
private static final MySingleton INSTANCE = new MySingleton();
private MySingleton() {}
public static MySingleton getInstance() {
return INSTANCE;
}
// 确保反序列化后返回的正是 INSTANCE
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
}
说明:
没有 readResolve 时,反序列化后会出现一个新对象,与原始单例按 == 比较不相等;使用 readResolve 时——总是返回 INSTANCE。
实际验证:
MySingleton s1 = MySingleton.getInstance();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.bin"));
out.writeObject(s1);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.bin"));
MySingleton s2 = (MySingleton) in.readObject();
in.close();
System.out.println(s1 == s2); // 如果有 readResolve 为 true;没有则为 false
writeReplace:序列化代理对象
有时对象过于“沉重”,或者包含敏感数据,或者根本不该以完整形态对外暴露。这种情况下,可以序列化一个“替身”——代理对象。
示例:
假设我们有一个类 User,其中包含私有密码。我们不希望密码被序列化。
import java.io.*;
public class User implements Serializable {
private String username;
private transient String password; // transient — 不会被序列化
public User(String username, String password) {
this.username = username;
this.password = password;
}
// 序列化 User 的替身:仅序列化 UserProxy
private Object writeReplace() throws ObjectStreamException {
return new UserProxy(username);
}
// 仅用于序列化的代理类
private static class UserProxy implements Serializable {
private String username;
public UserProxy(String username) {
this.username = username;
}
private Object readResolve() throws ObjectStreamException {
// 现实中无法恢复密码——返回一个密码为空的 User
return new User(username, "");
}
}
}
说明:
- 序列化时,User 会被替换为 UserProxy(不包含密码)。
- 反序列化时,UserProxy 会再变回 User(但密码为空)。
3. 不可变对象的自定义序列化
不可变对象通常使用私有的 final 字段且没有 setter。标准的 Java 序列化可以绕过这些限制,但有时通过 writeReplace/readResolve 更明确地控制过程更好。
示例:Value Object
import java.io.*;
public final class Money implements Serializable {
private final int amount;
private final String currency;
public Money(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
private Object writeReplace() throws ObjectStreamException {
return new MoneyProxy(amount, currency);
}
private static class MoneyProxy implements Serializable {
private final int amount;
private final String currency;
MoneyProxy(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
private Object readResolve() throws ObjectStreamException {
return new Money(amount, currency);
}
}
}
说明:
- 序列化时,Money 会被替换为 MoneyProxy(一个 POJO)。
- 反序列化时,MoneyProxy 会再变回 Money。
与 writeObject/readObject 的交互
writeReplace/readResolve 与 writeObject/readObject 相互独立。如果两套机制都定义了,会先调用 writeReplace,随后对返回的对象再调用 writeObject(前提是它实现了 Serializable)。
示意图:
flowchart LR
A[对象] -- writeReplace --> B[代理对象]
B -- writeObject --> C[字节流]
C -- readObject --> D[代理对象]
D -- readResolve --> E[最终对象]
4. 实践:带对象替换的序列化
让我们在你的练习应用中加入自定义序列化——例如对类 Person,让序列化时只写入姓名而忽略年龄(假设我们出于隐私考虑)。
步骤 1. 主类
import java.io.*;
public class Person implements Serializable {
private String name;
private int age; // 不希望被序列化
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private Object writeReplace() throws ObjectStreamException {
return new PersonProxy(name);
}
private static class PersonProxy implements Serializable {
private final String name;
PersonProxy(String name) {
this.name = name;
}
private Object readResolve() throws ObjectStreamException {
return new Person(name, -1); // -1 — "年龄未知"
}
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
步骤 2. 测试
public class TestCustomSerialization {
public static void main(String[] args) throws Exception {
Person original = new Person("Alice", 30);
// 序列化
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"));
out.writeObject(original);
out.close();
// 反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"));
Person deserialized = (Person) in.readObject();
in.close();
System.out.println("序列化之前: " + original);
System.out.println("反序列化之后: " + deserialized);
}
}
结果:
序列化之前: Person{name='Alice', age=30}
反序列化之后: Person{name='Alice', age=-1}
如你所见,年龄没有被序列化——一切按计划进行!
5. 特点与细节
何时使用 writeReplace/readResolve?
- 当只需序列化对象的部分状态时。
- 用于代理对象的序列化/反序列化。
- 用于支持模式 Singleton。
- 用于不可变或复杂对象,其内部结构可能发生变化。
何时不应使用?
- 若可通过 transient 字段或 writeObject/readObject 解决。
- 若对象不应被替换为其他对象。
与继承的兼容性
如果父类定义了 writeReplace/readResolve,则它们也会在子类上被调用(除非被重写)。在层次结构中要小心!
6. 自定义序列化的常见错误
错误 #1:方法可见性不正确。 如果将 writeReplace/readResolve 设为非 private,序列化机制不会调用它们。必须是 private!
错误 #2:返回类型不匹配。 writeReplace/readResolve 必须返回 Object。即使实际上返回的是你的自定义类型——方法也要声明为返回 Object。
错误 #3:数据丢失。 如果代理对象不包含恢复原始对象所需的全部数据,信息会丢失。务必确保能够将对象恢复回来。
错误 #4:破坏不变量。 readResolve 应该返回符合程序期望的对象(例如对于单例——必须返回 INSTANCE)。
错误 #5:未处理的异常。 writeReplace/readResolve 可能抛出 ObjectStreamException。请处理或显式抛出它。
GO TO FULL VERSION