序列化物件圖時,若多個欄位引用同一物件,或存在循環引用,直接寫入會導致:
TC_REFERENCE),否則完整序列化並登記。此機制完美解決共享引用與循環引用問題。
import java.io.*;
import java.util.*;
class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private double salary;
public Employee(String n, double s) { name=n; salary=s; }
public void raiseSalary(double pct) { salary *= (1+pct/100); }
@Override public String toString() {
return getClass().getName()+"[name="+name+",salary="+salary+"]";
}
}
class Manager extends Employee {
private static final long serialVersionUID = 1L;
private Employee secretary;
public Manager(String n, double s) { super(n,s); }
public void setSecretary(Employee s) { secretary=s; }
@Override public String toString() {
return super.toString()+"[secretary="+secretary+"]";
}
}
public class SerializationRefDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Employee harry = new Employee("Harry Hacker", 50000);
Manager boss = new Manager("Tony Tester", 80000);
boss.setSecretary(harry); // 共享引用
Employee[] staff = {harry, boss}; // harry 同時在陣列與 boss.secretary
// 序列化
try (ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("staff.ser"))) {
out.writeObject(staff);
}
// 反序列化
Employee[] newStaff;
try (ObjectInputStream in = new ObjectInputStream(
new FileInputStream("staff.ser"))) {
newStaff = (Employee[]) in.readObject();
}
// 驗證共享引用保持
newStaff[0].raiseSalary(10); // 透過陣列加薪
System.out.println("Harry (via array): " + newStaff[0]);
System.out.println("Boss secretary: " + ((Manager)newStaff[1]).getSecretary());
// 輸出薪資應同步變化,證明為同一物件
}
}
transient → 不序列化,反序列化後為預設值。Socket、Thread)、可重算欄位。
import java.awt.geom.Point2D;
import java.io.*;
class LabeledPoint implements Serializable {
private static final long serialVersionUID = 1L;
private String label;
private transient Point2D.Double point; // Point2D.Double 未實作 Serializable
public LabeledPoint(String label, double x, double y) {
this.label = label;
this.point = new Point2D.Double(x, y);
}
// 序列化:手動寫入座標
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 處理 label
out.writeDouble(point.x);
out.writeDouble(point.y);
}
// 反序列化:手動讀取並重建 Point2D.Double
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
double x = in.readDouble() + 1.0; // 示範:讀取時微調
double y = in.readDouble() + 1.0;
point = new Point2D.Double(x, y);
}
@Override public String toString() {
return "LabeledPoint[label="+label+", x="+point.x+", y="+point.y+"]";
}
}
public class TransientDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
LabeledPoint p = new LabeledPoint("Book", 5.0, 5.0);
System.out.println("原始: " + p);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("point.ser"))) {
out.writeObject(p);
}
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("point.ser"))) {
LabeledPoint p2 = (LabeledPoint) in.readObject();
System.out.println("反序列化: " + p2); // 座標會 +1.0
}
}
}
鐵律: 必須 private、簽名固定、呼叫 defaultWriteObject/ReadObject、讀寫順序一致。
class FullControl implements Externalizable {
private String data;
private int version;
public FullControl() {} // public 無參建構子強制
@Override public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(data);
out.writeInt(version);
}
@Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
data = in.readUTF();
version = in.readInt();
}
}
⚠ 風險: 方法為 public,惡意代碼可竄改流。敏感資料需加密/簽名。
| 場景 | 策略 |
|---|---|
| 類別新增欄位 | 相容(新欄位取預設值),建議宣告 serialVersionUID |
| 類別刪除欄位 | 相容(舊流多餘資料忽略),若欄位為 transient 需處理 |
| 修改欄位型別 | 高風險,建議新增欄位並 @Deprecated 舊欄位 |
| 跨 JVM 版本 | 固定 serialVersionUID,測試往返相容性 |
| 防範惡意反序列化 | 使用 ObjectInputFilter (JDK 9+)、避免反序列化不可信資料、改用 JSON/Protobuf |
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| Java 原生序列化 | 零配置、支援任意物件圖、深複製簡單 | 不安全、效能差、跨語言難、版本演進脆弱 | 舊系統維護、內部緩存、RMI |
| JSON (Jackson/Gson) | 人類可讀、跨語言、生態豐富、安全 | 需映射類別、不支援循環引用(需配置)、無型別資訊 | Web API、配置檔、異構系統整合 |
| Protocol Buffers | 極快、極小、Schema 演進強、多語言 | 需 .proto 定義、非人類可讀 | 高效能微服務、gRPC、大數據 |
| Kryo/FST | Java 專用、極快、支援循環引用 | 跨語言弱、需註冊類別 | Spark/Flink 內部、高效能緩存 |
Java 序列化以「handle 表 + 遞歸遍歷」優雅解決物件引用共享與循環問題。掌握 Serializable 預設行為、transient 跳過、自訂 writeObject/readObject 進階控制、Externalizable 完全自控,並理解 serialVersionUID、安全風險、版本演進策略,是處理分散式物件傳輸、持久化、深複製的核心技能。現代架構中,建議根據場景選擇 JSON、Protobuf、Kryo 等更安全、高效、跨語言的替代方案。