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