CodeGym /課程 /JAVA 25 SELF /控制序列化流程:writeObject、readObject

控制序列化流程:writeObject、readObject

JAVA 25 SELF
等級 43 , 課堂 0
開放

1. 介紹

自動序列化就像飛機上的自動駕駛:一切照著計畫進行時,它運作得很好。但一旦出現特殊情況,就會發現僅靠這個簡單機制已不夠。試想需要保存一個物件,但並非所有欄位都該被保存:有些資料是暫時性的,有些則過於敏感,不適合寫入檔案。或者反過來——保存時需要加入自己的資訊:例如版本或校驗和。有時在寫入或讀取資料之前,還需要先做檢查或轉換。更複雜的情況是:當類別結構隨時間變動時,仍需與舊版本保持相容。

在這些情境下就能體會到:僅靠預設序列化是不夠的。你得親自掌控流程。

序列化的特殊方法:writeObject 與 readObject

在 Java 中,有兩個特殊方法可讓你完全掌控物件的序列化與反序列化流程:

private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException

重要!

  • 方法必須是 private(不是 public、不是 protected、也不是 package-private)。
  • 簽名必須與上面所示完全一致。
  • 如果這些方法在你的類別中宣告了,JVM 將呼叫它們,而不是使用預設的序列化/反序列化。

運作原理?

當你呼叫 ObjectOutputStream.writeObject(obj) 時,JVM 會先在 obj 的類別中尋找方法 private void writeObject(ObjectOutputStream)。若存在,就會呼叫該方法。類似地,在反序列化時會呼叫 private void readObject(ObjectInputStream)。

若未宣告這些方法,將使用預設的序列化。

writeObject 與 readObject 的結構

方法簽名

private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException

在這些方法內你必須呼叫:

  • out.defaultWriteObject(); — 用於序列化父類別與目前類別的標準(非 transient)欄位。
  • in.defaultReadObject(); — 用於反序列化標準欄位。

如果不呼叫這些方法,標準欄位將不會被序列化——反序列化後物件會是「空的」。這就像出門忘了把護照放進行李:人雖到,但無法證明身分。

2. 範例:在序列化時加入校驗和

來看一個實際例子。假設我們有一個使用者類別,希望在序列化時把校驗和附加到物件,方便在反序列化時驗證資料完整性。

import java.io.*;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    // transient 欄位——不序列化它
    private transient int checksum;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
        this.checksum = calculateChecksum();
    }

    private int calculateChecksum() {
        return (name != null ? name.hashCode() : 0) + age;
    }

    // 自訂序列化
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject(); // 保存標準欄位
        int sum = calculateChecksum();
        out.writeInt(sum); // 寫入校驗和
        System.out.println("[LOG] 序列化 User: checksum=" + sum);
    }

    // 自訂反序列化
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject(); // 還原標準欄位
        int sum = in.readInt(); // 讀取校驗和
        int actual = calculateChecksum();
        System.out.println("[LOG] 反序列化 User: checksum=" + sum + ", actual=" + actual);
        if (sum != actual) {
            throw new IOException("資料已損壞!校驗和不相符。");
        }
        this.checksum = actual;
    }

    @Override
    public String toString() {
        return "User{name='" + name + "', age=" + age + ", checksum=" + checksum + "}";
    }
}

使用範例:

// 儲存物件
User user = new User("Alice", 42);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
    out.writeObject(user);
}

// 載入物件
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
    User loaded = (User) in.readObject();
    System.out.println("還原的物件:" + loaded);
}

發生了什麼?

  • 序列化時會呼叫 writeObject,保存標準欄位加上校驗和。
  • 反序列化時會呼叫 readObject,復原欄位並檢查校驗和。
  • 主控台會出現日誌;若有任何異常,將拋出例外。

3. 從序列化中排除敏感資料

有時需要讓某些欄位不要被序列化(例如密碼)。可用關鍵字 transient(詳細內容會在下一講),或者在自行實作 writeObject 時手動略過該欄位。

範例:

public class Account implements Serializable {
    private static final long serialVersionUID = 1L;
    private String username;
    private transient String password; // transient——不會被序列化

    // 也可以這麼做:
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        // 不寫入 password!
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        // password 會保持為 null
    }
}

注意:
如果只想序列化物件的一部分——就不要把多餘的欄位寫入串流。

4. 呼叫父類別方法:defaultWriteObject 與 defaultReadObject

在你的 writeObjectreadObject 方法中,幾乎總是需要呼叫 defaultWriteObject()defaultReadObject()。這有點像在加上自己的備註之前先按下「儲存草稿」。

這些方法負責將目前類別與其父類別中所有非 transient、非 static 的欄位依預設方式序列化。若不呼叫,這些欄位將不會被序列化,反序列化後會是空的。

錯誤示範:

private void writeObject(ObjectOutputStream out) throws IOException {
    // out.defaultWriteObject(); // 忘了呼叫!
    out.writeInt(123); // 自行寫入一些資料
}

在此情況下,標準欄位根本不會被保存!

5. 實作:為序列化流程加上日誌

讓我們在使用者類別中加入日誌,以便觀察何時發生序列化與反序列化。

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    private void writeObject(ObjectOutputStream out) throws IOException {
        System.out.println("[LOG] 序列化 Person: " + name + ", 年齡 " + age);
        out.defaultWriteObject();
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        System.out.println("[LOG] 反序列化 Person: " + name + ", 年齡 " + age);
    }
}

使用方式:

Person p = new Person("Bob", 30);
// 存到檔案
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"))) {
    out.writeObject(p);
}
// 從檔案載入
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"))) {
    Person loaded = (Person) in.readObject();
}

結果:
你會在主控台看到物件正在序列化與反序列化的訊息。

6. 使用 writeObject/readObject 時的常見錯誤

錯誤 №1: 未呼叫 defaultWriteObject/defaultReadObject 如果忘了呼叫這些方法,標準欄位不會被序列化,而物件在反序列化後會是空的或不正確。

錯誤 №2: 方法簽名不正確。 方法必須嚴格是 private void writeObject(ObjectOutputStream) 與 private void readObject(ObjectInputStream)。若將它們設為 public/protected 或更改參數——就不會被自動呼叫。

錯誤 №3: 方法中拋出了例外。 如果在 writeObjectreadObject 中發生例外,序列化或反序列化會被中斷,物件將無法正確保存/載入。

錯誤 №4: 忘了序列化/反序列化父類別。 如果你的類別繼承自其他可序列化的類別,務必呼叫 defaultWriteObject/defaultReadObject,否則父類別的欄位不會被保存。

錯誤 №5: 序列化了敏感資料。 若忘了排除密碼或其他私密資料,它們會被寫入序列化檔案。請使用 transient 或在手動邏輯中略過它們。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION