CodeGym /課程 /JAVA 25 SELF /Externalizable:序列化的精細調校

Externalizable:序列化的精細調校

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

1. 前言

在 Java 中,對物件進行序列化最常用的是介面 Serializable。它很簡單:只要實作該介面,就能透過 ObjectOutputStream/ObjectInputStream 來寫入/讀取物件。但有時這還不夠:

  • 需要完全控制要序列化哪些欄位,以及如何序列化。
  • 需要確保類別不同版本之間的相容性。
  • 想要減小序列化檔案的大小或加速序列化流程。

對此,Java 提供了 Externalizable 介面——這是一種更「手動」且更彈性的序列化方式。

簡言之:

  • Serializable —— 自動序列化:由 Java 自行決定要寫入什麼以及如何寫入。
  • Externalizable —— 手動序列化:你自行指定要如何保存/還原。

2. Externalizable 合約:實作 writeExternalreadExternal

要使用 Externalizable,需要:

  1. 實作介面 java.io.Externalizable
  2. 必須實作兩個方法:
    • void writeExternal(ObjectOutput out) throws IOException
    • void readExternal(ObjectInput in) throws IOException, ClassNotFoundException

範例:

import java.io.*;

public class User implements Externalizable {
    private String name;
    private int age;

    // 必須的 public 無參數建構子!
    public User() {}

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

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = in.readUTF();
        age = in.readInt();
    }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

重要:開發者可自行決定哪些欄位要被序列化以及其順序。不過有一個強制要求:類別必須有一個 public 的無參數建構子。若沒有,反序列化時程式會拋出 InvalidClassException

3. 何時使用 Externalizable

在以下情況請使用 Externalizable

  • 需要對資料格式有完全控制。 例如只序列化部分欄位,或以特定的順序/格式序列化。
  • 優化效能與檔案大小。 標準序列化會加入額外的資訊(中繼資料、類別名稱、型別等)。使用 Externalizable 時,你只會寫出所需的資料。
  • 確保向後相容性。 當類別結構變更時,可以手動實作讀取舊版與新版資料的邏輯。
  • 序列化非標準物件。 例如存在無法以標準方式序列化的欄位(如 transientvolatile,或複雜結構)。

何時不該使用?

  • 如果不需要完全控制——請使用 Serializable,它更簡單且更安全。
  • 如果無法保證在類別變更時能維持資料格式的相容性。

4. Externalizable 相對於 Serializable 的優缺點

優點:

  • 對序列化具有完全控制。 你自行決定要如何寫入/讀取。
  • 更精簡。 沒有多餘的中繼資料——只有你的資料。
  • 速度快。 資料更少——寫入/讀取更快。
  • 彈性高。 可以實作版本相容、加入壓縮、加密等。

缺點:

  • 需要手動實作——很容易出錯。 如果把寫入/讀取的順序搞混,序列化會「壞掉」(發生錯誤或資料不正確)。
  • 沒有自動支援 transientserialVersionUID。一切都要自行設計與實作。
  • 較難維護。 當類別結構改變時,必須記得更新序列化方法。
  • 必須有 public 無參數建構子。
  • 「魔法」更少——責任更大。

5. 範例:序列化與反序列化簡單物件

序列化物件

User user = new User("Alice", 30);

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); // Alice (30)
}

注意:如果你更動欄位的寫入/讀取順序,或忘了序列化某個欄位,資料就會不正確!writeExternalreadExternal 的操作順序必須嚴格一致。

範例:只序列化部分欄位

public class SecretUser implements Externalizable {
    private String login;
    private transient String password; // 在 Externalizable 中 transient 不起作用

    public SecretUser() {}

    public SecretUser(String login, String password) {
        this.login = login;
        this.password = password;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(login);
        // 不序列化密碼!
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        login = in.readUTF();
        password = null; // 不還原密碼
    }
}

6. 實作:比較序列化檔案大小

來比較透過 SerializableExternalizable 序列化後,檔案有多「大」。

使用 Serializable 的類別

public class UserSerializable implements Serializable {
    private String name;
    private int age;

    public UserSerializable(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

使用 Externalizable 的類別

public class UserExternalizable implements Externalizable {
    private String name;
    private int age;

    public UserExternalizable() {}

    public UserExternalizable(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = in.readUTF();
        age = in.readInt();
    }
}

比較用程式碼

import java.io.*;

public class CompareSerialization {
    public static void main(String[] args) throws Exception {
        UserSerializable s = new UserSerializable("Bob", 25);
        UserExternalizable e = new UserExternalizable("Bob", 25);

        // Serializable
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin"))) {
            out.writeObject(s);
        }

        // Externalizable
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ext.bin"))) {
            out.writeObject(e);
        }

        System.out.println("Serializable file size: " + new File("ser.bin").length());
        System.out.println("Externalizable file size: " + new File("ext.bin").length());
    }
}

結果:
檔案 ser.bin(Serializable)通常較大——包含 Java 的中繼資料。檔案 ext.bin(Externalizable)只包含你的資料,通常較小。

7. 使用 Externalizable 時的常見錯誤

錯誤 №1:缺少 public 無參數建構子。
實作 Externalizable 的類別一定要有 public 無參數建構子。少了它,反序列化會拋出 InvalidClassException

錯誤 №2:讀寫欄位的順序不一致。
方法 writeExternalreadExternal 必須以相同順序運作。如果寫入時先保存欄位 name,但讀取時先嘗試讀取 age,資料就會被破壞。

錯誤 №3:序列化時遺漏欄位。
如果在 writeExternal 中忘了寫入某個欄位,反序列化後它會是 null(對於參考型別)或 0(對於數值型別)。

錯誤 №4:錯誤使用 transientserialVersionUID
Serializable 不同,對 Externalizable 而言這些機制不會自動生效——你必須自行控制哪些欄位要保存、哪些不要。

錯誤 №5:變更類別結構卻未更新方法。
如果新增或刪除欄位,卻沒有在 writeExternalreadExternal 中做相應調整,舊有的已保存資料可能無法正確載入。

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