Java 序列化是將物件狀態轉為位元組流(持久化/網絡傳輸/分散式會話),反序列化還原物件。看似簡單的 `implements Serializable` 背後,隱藏著預設機制、效能陷阱、安全漏洞、版本演進等硬核課題。本文系統梳理核心原理、四種定制化方式、單例破壞與修復、最佳實踐。
`ObjectOutputStream.writeObject(obj)` 觸發深度優先遍歷:
效能警示: 物件圖龐大/深層嵌套/大量集合嵌套集合 → 序列化耗時、位元組流膨脹、棧溢出風險。
static 欄位屬於類,不屬於實例,不序列化。transient 顯式排除(如敏感密碼、臨時緩存、非序列化第三方物件)。
public class User implements Serializable {
private String username;
private transient String password; // 不序列化
private transient String encryptedPassword; // 衍生欄位
// 序列化前:加密/轉換
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 先執行預設序列化 (username)
this.encryptedPassword = encrypt(password); // 衍生加密欄位
out.writeObject(encryptedPassword); // 顯式寫入
}
// 反序列化後:還原/解密
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 讀 username
this.encryptedPassword = (String) in.readObject(); // 讀加密密碼
this.password = decrypt(encryptedPassword); // 還原明文 (或保持加密)
}
}
適用: 少數欄位需特殊處理,主體仍用預設機制。
public class UserProfile implements Externalizable {
private String name;
private int age;
private Set tags = new HashSet<>();
public UserProfile() {} // 必須有 public 無參建構子!
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
out.writeObject(tags); // 完全自定義順序、格式、壓縮
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.name = (String) in.readObject();
this.age = in.readInt();
this.tags = (Set) in.readObject();
}
}
關鍵差異:
public 無參建構子 創建實例,再填充欄位。
// 顯式宣告,避免編譯器依賴類結構生成(修改類導致 UID 變化,反序列化拋 InvalidClassException)
private static final long serialVersionUID = 1L;
// 兼容性規則:
// 新增欄位:相容(新欄位取預設值)
// 刪除欄位:相容(舊資料忽略)
// 修改欄位型別:不相容
// 修改 serialVersionUID:強制不相容
public class Singleton implements Serializable {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() { return INSTANCE; }
// 反序列化時,JVM 會調用此方法,返回的物件替換新建實例
protected Object readResolve() {
return INSTANCE; // 確保單例唯一性
}
}
原理: 反序列化創建新實例後,若存在 `readResolve`,其返回值直接替換新實例(新實例變垃圾)。枚舉天然支援此機制,枚舉是實現單例的最佳方式(防反射、防序列化破壞)。
無參建構子 初始化父類欄位(必須有無參建構子,否則拋 `InvalidClassException`)。針對不可變類或複雜不變約束,引入內部私有 `SerializationProxy`:
public class Period implements Serializable {
private final Date start;
private final Date end;
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeObject(new SerializationProxy(this)); // 寫入代理
}
private void readObject(ObjectInputStream in) throws IOException {
throw new InvalidObjectException("Proxy required"); // 禁止直接反序列化
}
private Object readResolve() {
return ((SerializationProxy) in.readObject()).resolve(); // 代理還原
}
private static class SerializationProxy implements Serializable {
private final Date start, end;
SerializationProxy(Period p) { this.start = p.start; this.end = p.end; }
Object resolve() { return new Period(start, end); }
}
}
ObjectInputFilter (Java 9+)、白名單限制可反序列化類、避免反序列化不可信資料、使用 JSON/Protobuf 替代。| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| Java 原生序列化 | 零依賴、支援任意物件圖 | 體積大、速度慢、不跨語言、安全風險 | 臨時緩存、單機持久化、舊系統相容 |
| JSON (Jackson/Gson) | 可讀、跨語言、生態成熟 | 無類型資訊、效能中等 | REST API、配置檔、日誌 |
| Protocol Buffers | 極小體積、極快、Schema 演進、跨語言 | 需 .proto 定義、學習曲線 | 高效能 RPC、微服務通訊、大數據存儲 |
| Kryo / FST | 比原生快 5-10x、體積小、支援循環引用 | 第三方依賴、非標準 | 分散式快取、Spark/Kafka 內部序列化 |
Java 序列化是把雙刃劍:用得好是分散式系統潤滑劑,用不好是效能殺手與安全漏洞。理解「預設機制如何遍歷物件圖」、「四種定制化切入點」、「版本演進規則」、「單例保護原理」,才能在遺留系統維護、框架原理分析、安全審計中遊刃有餘。下一篇將實戰《Jackson/Protobuf 遷移指南與效能基準測試》。