團隊將用戶設定從 Hashtable 遷移至 HashMap,發現序列化資料不相容。原以為必須寫轉換工具,實則 Java 序列化原生支援類別演進。本文揭露 5 個鮮為人知卻極實用的特性。
自動相容的變更: 新增欄位、static→非 static、transient→非 transient。
需額外處理: 刪除欄位、非 static→static、非 transient→transient。
關鍵在於 serialVersionUID。若未顯式宣告,Java 依類別結構算出 hash,任何修改皆導致 InvalidClassException。解法:
// 取得原始版本 UID
$ serialver com.tedneward.Person
// 在新版本類別中固定宣告
private static final long serialVersionUID = -1234567890L;
固定 UID 後,PersonV2 新增 gender 欄位,反序列化舊資料時自動設為預設值 (null);反向亦可。
二進位格式完全文檔化,private 欄位在 RMI 傳輸中明文可見。對策:實作 writeObject/readObject 鉤子進行混淆/加密。
private void writeObject(ObjectOutputStream out) throws IOException {
age <<= 2; // 簡單位移混淆
out.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age >>= 2; // 還原
}
注意: 方法簽名必須精確一致,否則無聲失效。
若需完整加密與驗證,優於手工實作:
javax.crypto.SealedObject:對稱加密封裝,需單獨管理金鑰。java.security.SignedObject:數位簽名驗證完整性。new SealedObject(new SignedObject(obj, key), cipher)。若物件核心資料可推導其餘欄位,可序列化輕量級 Proxy 替身:
writeReplace():回傳 Proxy 物件寫入流。readResolve():反序列化時由 Proxy 還原真實物件。class PersonProxy implements Serializable {
String data; // 扁平化核心資料
Object readResolve() { /* 解析 data 重建 Person */ }
}
class Person implements Serializable {
Object writeReplace() { return new PersonProxy(this); }
}
此技巧不需讀寫平衡,舊版本可只實作 readResolve 升級為新類型。
反序列化後自動驗證欄位合法性:
public class Person implements Serializable, ObjectInputValidation {
public void validateObject() throws InvalidObjectException {
if (age < 0 || age > 150) throw new InvalidObjectException("Invalid age");
}
}
若驗證失敗,拋出 InvalidObjectException 中止還原。
Java 序列化遠比想像靈活:版本演進、安全鉤子、內建加密簽名、代理替身、驗證機制。善用這些特性,能優雅解決棘手的持久化與傳輸難題。下期預告:Java Collections 5 件事。