Java 序列化將物件狀態轉為位元組流,支援持久化與網路傳輸。預設機制雖方便,但暗藏效能陷阱與版本演進地雷。本文深入剖析預設行為、transient 控制、serialVersionUID 版本策略與相容性實戰。
實作 Serializable 介面 (標記介面,無方法) 即啟用預設序列化。JVM 會遞迴走訪物件圖,將所有非 transient、非 static 欄位寫入流中。
public class Employee implements Serializable {
private int id;
private String name;
private transient String password; // 不序列化
private static final String COMPANY = "Acme"; // static 不序列化
}
StackOverflowError。標記 transient 欄位將被跳過。反序列化時,該欄位獲得預設值 (null/0/false)。責任轉移:你必須在 readObject 中自行初始化。
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 先執行預設反序列化
this.password = loadPasswordFromSecureStore(); // 手動初始化 transient 欄位
}
JVM 依據類別結構自動計算 UID (長整數),任何結構變更 (增減欄位/方法、修改存取修飾符) 都會導致 UID 變動,反序列化拋出 InvalidClassException。
| 策略 | 做法 | 適用場景 | 風險 |
|---|---|---|---|
| 強制不相容 | 顯式宣告新 UID (private static final long serialVersionUID = 2L;) | 重大架構重構、資料格式全面變更、舊資料不可用 | 舊資料永久無法讀取 |
| 向下相容 (舊讀新) | 保持 UID 不變,只新增非私有欄位/方法,舊版反序列化忽略新欄位 | 輕量擴充、新增輔助功能 | 新欄位在舊版為預設值,業務邏輯需容忍 |
| 向上相容 (新讀舊) | 保持 UID 不變,新增欄位並提供合理預設值,readObject 處理缺失欄位 | 服務端升級、客戶端滾動升級期間 | 舊資料缺失新欄位,需防禦性編程 |
// v1
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 固定 UID
private String name;
}
// v2 新增欄位
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 同 UID
private String name;
private String email; // 新欄位
}
v1 序列化資料 → v2 反序列化:成功,email = null。
v2 序列化資料 → v1 反序列化:成功,email 被忽略。
實作 writeObject/readObject (私有方法) 繞過預設機制,適用於:
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeObject(encrypt(this.password)); // 自訂加密寫入
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.password = decrypt((String) in.readObject()); // 配對讀取
}
serialVersionUID,避免跨 JVM/編譯器差異。readObject 初始化。序列化看似簡單,實則是分散式系統、快取、持久化層的核心基礎設施。深入理解其機制與演進策略,是打造穩健 Java 系統的必修課。