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