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);
}
}
注意:採用此作法時,反序列化需要知道寫入了多少個物件(或使用特定的「終止標記」)。
GO TO FULL VERSION