首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

面向物件與 Java 序列化:機制、引用處理、自訂策略與實戰陷阱

     編輯:Java 相關知識

序列化核心問題:物件引用與重複

序列化物件圖時,若多個欄位引用同一物件,或存在循環引用,直接寫入會導致:

  1. 資料冗余(重複序列化同一物件)
  2. 反序列化後破壞共享引用關係(變成多個副本)
  3. 無限遞歸(StackOverflowError)

Java 序列化機制的解決方案

  1. 序列號機制:每個被序列化物件分配唯一 handle(序列號)。
  2. 引用檢查:寫入前查 handle 表,已存在則寫入參考標記(TC_REFERENCE),否則完整序列化並登記。
  3. 反序列化還原:讀取 handle,首次建立物件並登記,後續遇參考標記直接返回已建立實例。

此機制完美解決共享引用與循環引用問題。

基礎實戰:Employee/Manager 共享引用範例


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 與自訂 writeObject/readObject

1. transient 跳過欄位

  • 標記欄位 transient → 不序列化,反序列化後為預設值。
  • 適用:敏感資料、非序列化物件(Socket、Thread)、可重算欄位。

2. 自訂序列化邏輯


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、讀寫順序一致。

Externalizable:完全自控模式


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/FSTJava 專用、極快、支援循環引用跨語言弱、需註冊類別Spark/Flink 內部、高效能緩存

總結

Java 序列化以「handle 表 + 遞歸遍歷」優雅解決物件引用共享與循環問題。掌握 Serializable 預設行為、transient 跳過、自訂 writeObject/readObject 進階控制、Externalizable 完全自控,並理解 serialVersionUID、安全風險、版本演進策略,是處理分散式物件傳輸、持久化、深複製的核心技能。現代架構中,建議根據場景選擇 JSON、Protobuf、Kryo 等更安全、高效、跨語言的替代方案。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved