預設的 Java 序列化機制雖然簡單易用,但當遇到特殊需求時——例如出於安全考量不想序列化對象的某些部分,或某些子對象在恢復後需要重新創建——就需要更精細的控制。此時實作 Externalizable 介面便能完全掌控序列化與反序列化的具體流程。
Externalizable 繼承自 Serializable,並新增兩個關鍵方法:writeExternal() 與 readExternal()。序列化與反序列化過程中會自動呼叫這兩個方法,讓開發者執行特殊操作。
以下範例展示 Blip1 與 Blip2 的細微差別:
class Blip1 implements Externalizable {
public Blip1() { System.out.println("Blip1 Constructor"); }
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("Blip1.writeExternal");
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
System.out.println("Blip1.readExternal");
}
}
class Blip2 implements Externalizable {
Blip2() { System.out.println("Blip2 Constructor"); } // 非 public
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("Blip2.writeExternal");
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
System.out.println("Blip2.readExternal");
}
}
執行結果顯示 Blip1 成功恢復,但 Blip2 拋出異常。原因在於 Blip2 的建構子非 public,反序列化時 JVM 無法呼叫預設建構子。Externalizable 物件反序列化會先呼叫預設建構子,再呼叫 readExternal(),這與 Serializable 直接從二進制位恢復、不呼叫建構子有本質不同。
class Blip3 implements Externalizable {
int i; String s;
public Blip3() { System.out.println("Blip3 Constructor"); }
public Blip3(String x, int a) { s = x; i = a; }
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(s); out.writeInt(i); // 必須顯式寫入
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
s = (String)in.readObject(); i = in.readInt(); // 必須顯式讀取
}
}
若不在 readExternal 中初始化欄位,它們將保持預設值(null 或 0)。繼承 Externalizable 物件時,通常需呼叫基底類別的 writeExternal() 與 readExternal() 以正確處理基底部分。
若僅需防止特定敏感欄位(如密碼)被序列化,可使用 transient 關鍵字標記該欄位,意為「自動機制勿需保存/恢復此欄位,我自行處理」。
class Logon implements Serializable {
private Date date = new Date();
private String username;
private transient String password; // 不會被自動序列化
// ...
}
反序列化後,password 將為 null。注意 transient 僅適用於 Serializable,Externalizable 預設不序列化任何欄位,故無需此關鍵字。
實作 Serializable 並新增 private void writeObject(ObjectOutputStream) 與 private void readObject(ObjectInputStream) 方法,即可接管序列化流程。這兩個方法簽名必須精確:
private void writeObject(ObjectOutputStream stream) throws IOException;
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException;
雖然它們是 private,但序列化機制會透過反射呼叫。在方法內可呼叫 defaultWriteObject() 與 defaultReadObject() 執行預設行為,再手動處理 transient 欄位:
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject(); // 處理非 transient 欄位
stream.writeObject(b); // 手動處理 transient 欄位
}
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
b = (String)stream.readObject();
}
此方式比 Externalizable 更靈活,可選擇性保留預設序列化行為。
可序列化類別的版本演進雖受支援,但需深入理解底層機制,建議參考 JDK 文件中的 serialVersionUID 等進階主題。