CodeGym /課程 /JAVA 25 SELF /解析集合序列化的常見錯誤

解析集合序列化的常見錯誤

JAVA 25 SELF
等級 44 , 課堂 4
開放

1. NotSerializableException:當集合無法序列化

在序列化集合時最常見且最難纏的錯誤,就是 java.io.NotSerializableException。只要集合中有任一元素沒有實作 Serializable 介面,就會發生。

來看一個簡單的範例:

import java.io.*;
import java.util.*;

class Book {
    String title;
    Book(String title) { this.title = title; }
}

public class LibraryApp {
    public static void main(String[] args) throws Exception {
        List<Book> books = new ArrayList<>();
        books.add(new Book("董貝父子"));

        // 嘗試序列化集合
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("books.ser"))) {
            oos.writeObject(books); // 砰!NotSerializableException
        }
    }
}

會發生什麼?在 oos.writeObject(books) 這一步,你會得到例外:

java.io.NotSerializableException: Book

為什麼?因為 Book 類別沒有實作 Serializable 介面。就算集合本身(ArrayList)可以被序列化,集合中的元素也必須是可序列化的!

如何診斷

錯誤訊息中總會指出導致問題的類別——在例外訊息裡找它。如果集合很大,且只在特定情況下出錯,可能是某個元素被意外加入且沒有實作 Serializable

如何修正

在你的類別上加入 implements Serializable

class Book implements Serializable {
    String title;
    Book(String title) { this.title = title; }
}

提示:如果集合包含不同型別的物件,請全部檢查是否符合 Serializable

2. 反序列化時的 ClassCastException:當泛型出問題

在 Java 中,集合的泛型參數會在編譯後被移除(type erasure)。這代表如果你序列化了 List<String>,卻以 List<Integer> 來反序列化,編譯器不會發現問題,但在執行期你會得到 ClassCastException

範例:

// 序列化
List<String> names = Arrays.asList("安娜", "鮑里斯");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("names.ser"))) {
    oos.writeObject(names);
}

// 反序列化(危險!)
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("names.ser"))) {
    List<Integer> numbers = (List<Integer>) ois.readObject(); // unchecked cast
    Integer first = numbers.get(0); // 砰!ClassCastException
}

錯誤:

java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer

如何避免

  • 不要使用「原始型別」的集合(raw types),也不要在非必要時進行型別轉換。
  • 如果不確定內容,反序列化之後請檢查元素的型別。
  • 文件化說明:寫清楚序列化的是什麼型別的集合,以及讀取時預期的型別。

安全的反序列化範例:

Object obj = ois.readObject();
if (obj instanceof List<?>) {
    List<?> list = (List<?>) obj;
    if (!list.isEmpty() && list.get(0) instanceof String) {
        @SuppressWarnings("unchecked")
        List<String> safeNames = (List<String>) obj; // 已抑制警告,但已檢查型別!
    }
}

3. 修改類別結構:serialVersionUID 與向後相容性

你序列化了集合,之後決定在元素類別中新增欄位、修改欄位名稱,甚至改變類別結構。此時嘗試反序列化舊檔案,會遇到神祕的錯誤:

java.io.InvalidClassException: Book; local class incompatible: stream classdesc serialVersionUID = 1234, local class serialVersionUID = 5678

為什麼會這樣

每個可序列化的類別都有一個唯一的版本識別碼——serialVersionUID。如果類別改變(例如你新增了欄位),JVM 會計算新的 serialVersionUID,反序列化時便會發現版本與序列化當時不一致。

如何避免

  • 在你的類別中明確宣告 serialVersionUID
class Book implements Serializable {
    private static final long serialVersionUID = 1L;
    String title;
    // ...
}
  • 維持向後相容:若計畫讀取舊檔案,請不要刪除或重新命名欄位。
  • 在修改後測試反序列化流程。

如果確實需要修改類別,該怎麼辦?

  • 考慮實作 readObject/writeObject 方法以手動控制序列化。
  • 或進行資料遷移:使用舊版類別讀取舊檔,再以新格式重新儲存。

4. 序列化不可變集合時的資料遺失

在新版 Java 中出現了不可變集合,例如透過 List.of()Set.of()Map.of() 建立。在舊版 Java(12 之前)以及某些第三方實作中,這類集合的序列化可能不夠正確:反序列化後集合會變成可變的,或甚至直接拋出錯誤。

範例:

List<String> list = List.of("a", "b", "c");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.ser"))) {
    oos.writeObject(list);
}

在較舊的 JVM 上,反序列化時可能出現錯誤,或集合不再維持不可變。

如何避免

  • 查閱你所使用 Java 版本的文件。
  • 對這類集合進行序列化與反序列化測試。
  • 如果必須保留不可變性,反序列化後以 Collections.unmodifiableList(list) 封裝。

5. 序列化 transient 與 static 欄位

這些欄位會如何處理:

  • transient —— 以此關鍵字標記的欄位完全不會被序列化。反序列化後會是預設值(例如 null 或 0)。
  • static —— 類別欄位(而非物件欄位)永遠不會被序列化。

範例:

class Book implements Serializable {
    String title;
    transient String cache; // 不會被序列化!
    static String publisher = "Default"; // 也不會被序列化!
}

為什麼這很重要

如果你在物件中保存某些可計算的值或快取,請將它們標記為 transient —— 這能節省空間並加速序列化。

注意:反序列化之後,transient 欄位需要重新計算或再次初始化。

6. 序列化大型集合:效能與檔案大小

問題:

  • 大型集合(例如百萬個物件)可能導致檔案極大、讀寫時間過長,甚至引發記憶體不足(OutOfMemoryError)。
  • 當序列化物件圖(例如複雜的相互關聯集合)時,檔案大小可能意外暴增。

如何避免

  • 分批序列化集合:例如一次寫入一個物件或小批次寫入。
  • 使用串流式處理:不要一次序列化整個集合,而是視需求逐一序列化元素。
  • 壓縮檔案:使用 GZIPOutputStream 以減小檔案大小。

串流式序列化範例:

try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("books.ser"))) {
    for (Book book : bigList) {
        oos.writeObject(book);
    }
}

注意:採用此作法時,反序列化需要知道寫入了多少個物件(或使用特定的「終止標記」)。

1
問卷/小測驗
序列化複雜結構,等級 44,課堂 4
未開放
序列化複雜結構
序列化複雜結構
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION