物件序列化是將物件狀態轉換為位元組流的過程;反序列化則是從位元組流重建物件。此機制讓物件能:
| 類別/介面 | 角色 | 關鍵方法 |
|---|---|---|
java.io.Serializable | 標記介面,無方法 | — |
java.io.Externalizable | 完全自控序列化 | writeExternal, readExternal |
ObjectOutputStream | 序列化輸出 | writeObject(Object), defaultWriteObject() |
ObjectInputStream | 反序列化輸入 | readObject(), defaultReadObject() |
import java.io.*;
import java.util.Date;
// 1. 實作 Serializable(標記介面)
class Employee implements Serializable {
private static final long serialVersionUID = 1L; // 強烈建議顯式宣告
private String name;
private double salary;
private transient Date hireDate; // transient 不序列化
public Employee(String name, double salary, Date hireDate) {
this.name = name; this.salary = salary; this.hireDate = hireDate;
}
// getters/setters/toString 省略
}
public class SerializationBasics {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Employee emp = new Employee("Alice", 85000, new Date());
// --- 序列化寫入檔案 ---
try (ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("employee.ser"))) {
out.writeObject(emp);
System.out.println("序列化完成");
}
// --- 反序列化讀取 ---
try (ObjectInputStream in = new ObjectInputStream(
new FileInputStream("employee.ser"))) {
Employee restored = (Employee) in.readObject();
System.out.println("反序列化結果: " + restored);
// 注意:hireDate 為 null(transient 跳過)
}
}
}
當物件包含非序列化欄位(如 Socket、Thread、第三方庫物件)或需加密/壓縮敏感欄位時,需實作 writeObject / readObject。
class ConnectionWrapper implements Serializable {
private static final long serialVersionUID = 1L;
private String host;
private int port;
private transient Socket socket; // 非序列化
public ConnectionWrapper(String host, int port) {
this.host = host; this.port = port;
this.socket = new Socket(host, port);
}
// 序列化時:儲存重建連線所需資訊
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 先處理非 transient 欄位
out.writeObject(host);
out.writeInt(port);
}
// 反序列化時:重建連線
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.host = (String) in.readObject();
this.port = in.readInt();
this.socket = new Socket(host, port); // 重新連線
}
}
關鍵規則:
private,由 JVM 自動調用。defaultWriteObject() / defaultReadObject() 處理標準欄位。實作 Externalizable 介面,類別需自行處理所有欄位(含父類別),JVM 僅寫入類別身份資訊。
class FullControl implements Externalizable {
private String data;
private int version;
public FullControl() {} // 必須有無參建構子
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(data);
out.writeInt(version);
// 可加入加密、壓縮、版本控制邏輯
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.data = (String) in.readObject();
this.version = in.readInt();
// 解密、解壓縮、版本遷移
}
}
⚠ 安全警示: writeExternal/readExternal 為 public,惡意代碼可竄改序列化資料。傳輸敏感資料時務必加密或使用安全通道。
ObjectOutputStream 維護一份「已序列化物件表」。同一物件圖中多次引用同一實例時,僅第一次寫入完整物件,後續寫入參考標記,避免重複、解決循環引用。
InvalidClassException。private static final long serialVersionUID = 1L;,避免編譯器自動生成導致跨版本不相容。Serializable → 子類別自動可序列化。| 陷阱 | 現象 | 解決方案 |
|---|---|---|
| 忘記宣告 serialVersionUID | 跨 JVM/版本反序列化失敗 | 顯式宣告固定值 |
| static/transient 欄位預期被序列化 | 反序列化後為預設值 | 理解修飾語語義,必要時自訂 writeObject |
| 序列化非最終類別被惡意繼承 | 安全風險 | 敏感類別用 final 或 readObject 驗證 |
| 大量物件序列化導致 OOM | 記憶體飆升 | 分批序列化、使用緩衝流、考慮替代格式 |
| 修改類別結構未更新 UID | InvalidClassException | 維護 UID 版本策略 |
雖然 Java 原生序列化功能強大,但存在安全漏洞、效能較差、跨語言困難等問題。現代專案建議考慮:
Java 序列化是分散式系統、持久化框架的基石。掌握 Serializable 基礎用法、transient 與自訂 writeObject/readObject 進階技巧、Externalizable 完全掌控模式,並理解 serialVersionUID、引用去重、繼承關係等底層機制,能讓你在處理物件傳輸、快取、深複製等場景時遊刃有餘。同時,務必評估安全風險,適時引入現代序列化方案替代。