CodeGym /課程 /JAVA 25 SELF /泛型集合的序列化:特性與注意事項

泛型集合的序列化:特性與注意事項

JAVA 25 SELF
等級 45 , 課堂 1
開放

1. 泛型集合的序列化與反序列化

Java 的泛型並不是魔法,而是由編譯器支撐的「假象」。在編譯階段,泛型型別參數的資訊會被擦除(稱為 type erasure,或「型別擦除」)。也就是說,在執行期(runtime)List<String>List<Object>List<Integer> 沒有差別。它們都只是 List,JVM 並不知道裡面實際放了哪些型別。

來看個例子:

List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

System.out.println(stringList.getClass() == intList.getClass()); // true!

這裡的機制有點巧妙。我們建立了兩個清單——一個給字串,一個給數字。在編譯階段,Java 會嚴格檢查,不允許你把非字串放進 List<String>,或把非整數放進 List<Integer>。但程式一旦執行,這些差異就消失了。對 JVM 而言,兩個物件都只是 ArrayList,它已經無法再檢查應該儲存哪種元素。正因如此,兩個清單類別的比較(stringList.getClass() == intList.getClass())會回傳 true

因此有個重要結論:Java 的泛型主要是在編譯階段提供便利與型別安全,但在執行期這些「標籤」會消失。所以當你序列化集合時,寫入檔案的只有資料本身,而沒有泛型型別的資訊。也就是說,儲存的是一份值的清單,但僅從檔案無法判斷它原本是 List<String>List<Object> 還是 List<Integer>

再一個例子:序列化與反序列化 List<String>

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

public class GenericSerializationDemo {
    public static void main(String[] args) throws Exception {
        List<String> fruits = new ArrayList<>();
        fruits.add("蘋果");
        fruits.add("香蕉");
        fruits.add("橘子");

        // 序列化
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("fruits.ser"))) {
            oos.writeObject(fruits);
        }

        // 反序列化
        List<String> loadedFruits;
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("fruits.ser"))) {
            loadedFruits = (List<String>) ois.readObject();
        }

        System.out.println(loadedFruits); // [蘋果, 香蕉, 橘子]
    }
}

請注意這一行:

loadedFruits = (List<String>) ois.readObject();

這裡我們將結果顯式轉型為 List<String>,但在執行期它其實只是 ArrayList。編譯器無法驗證它真的是字串清單,如果裡面出現不是字串的元素——就會在程式執行時拋出 ClassCastException

2. 反序列化泛型集合時的問題

元素型別資訊的遺失

由於泛型參數資訊會被擦除,反序列化之後 Java 無法保證集合中的物件就是你預期的型別。你拿到的只是「原始型別」(raw type)的集合,編譯器可能不會報錯,但問題會在執行期浮現。

問題示範

List rawList = new ArrayList();
rawList.add("貓");
rawList.add(42); // Integer!
// 反序列化
List<String> loadedCats = (List<String>) ois.readObject();
String cat = loadedCats.get(1); // ClassCastException!

Unchecked cast warning

編譯器會誠實地警告潛在問題:

Note: GenericSerializationDemo.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

這個警告表示你在未經檢查的情況下進行了轉型,集合中可能包含非預期型別的物件。

3. 序列化泛型集合的特性

序列化檔案中不包含泛型參數資訊

當你序列化 List<String>List<Integer> 時,檔案裡不會有任何它們是字串或整數的資訊。集合的內容會「如實」序列化——依序寫入物件。

若你用文字編輯器打開序列化檔案,也看不到任何關於 <String><Integer> 的字樣。這些只存在於原始碼與編譯器層面。

範例:序列化不同的集合

List<Integer> numbers = Arrays.asList(1, 2, 3);
List<String> words = Arrays.asList("一", "二", "三");

// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"))) {
    oos.writeObject(numbers);
    oos.writeObject(words);
}

在檔案 test.ser 裡只是兩個 ArrayList 物件,沒有任何泛型參數的資訊。

反序列化「原始型別」集合的問題

如果你以沒有泛型參數(raw type)的清單進行序列化,卻在反序列化時當作 List<String> 使用,編譯器無法檢查型別正確性,執行期可能會出錯。

4. 序列化泛型集合的最佳實務

文件化預期的元素型別。
如果你的 API 會序列化集合,務必註明預期的元素型別。例如:「此方法回傳序列化後的 List<User>」。

在反序列化後檢查元素型別。
反序列化集合之後,最好檢查所有元素是否為預期型別(特別是當資料來源不受你控制時)。

for (Object obj : loadedList) {
    if (!(obj instanceof String)) {
        throw new IllegalStateException("預期為字串,但找到: " + obj.getClass());
    }
}

使用不可變集合。
若你序列化的是唯讀集合,請使用不可變集合——例如 List.copyOfCollections.unmodifiableList。這有助於避免反序列化後的資料被不小心修改。

不要在同一個集合中混用多種型別。
盡量不要序列化含有不同型別元素的集合(例如內含多種類別的 List<Object>)。這會讓反序列化變得複雜,並可能導致錯誤。

謹慎使用警告抑制。
如果你確定反序列化得到的集合元素型別正確,可以用註解 @SuppressWarnings("unchecked") 來抑制編譯器警告:

@SuppressWarnings("unchecked")
List<String> loaded = (List<String>) ois.readObject();

但請審慎使用——很容易把問題一路隱藏到生產環境才爆出來。

5. 範例:序列化與反序列化含自訂類別的集合

假設我們有一個 User 類別:

import java.io.Serializable;

public class User implements Serializable {
    private String name;
    private int age;
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String toString() {
        return name + " (" + age + ")";
    }
}

序列化使用者清單:

List<User> users = Arrays.asList(
    new User("愛麗絲", 30),
    new User("鮑伯", 25)
);

// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users.ser"))) {
    oos.writeObject(users);
}

// 反序列化
List<User> loadedUsers;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("users.ser"))) {
    loadedUsers = (List<User>) ois.readObject();
}

System.out.println(loadedUsers); // [愛麗絲 (30), 鮑伯 (25)]

一切正常! 不過,如果有人在序列化檔案中夾帶了其他類別的物件,你在把元素當作 User 讀取時,就可能得到 ClassCastException

6. 巢狀泛型集合的序列化

集合可以是巢狀的,例如:List<List<String>>Map<String, List<User>> 等等。Java 會以遞迴方式序列化這類結構,但規則不變:

  • 所有巢狀集合與其元素都必須是可序列化的。
  • 泛型參數資訊依舊會被擦除。

範例:序列化清單的清單

List<List<String>> matrix = new ArrayList<>();
matrix.add(Arrays.asList("a", "b"));
matrix.add(Arrays.asList("c", "d"));

// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("matrix.ser"))) {
    oos.writeObject(matrix);
}

// 反序列化
List<List<String>> loadedMatrix;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("matrix.ser"))) {
    loadedMatrix = (List<List<String>>) ois.readObject();
}

System.out.println(loadedMatrix); // [[a, b], [c, d]]

7. 實用細節

以不同實作序列化泛型集合

有時你以某種集合實作進行序列化,卻在反序列化時轉成另一種。例如,序列化的是 ArrayList,反序列化時卻轉為 LinkedList。這會導致型別轉換錯誤:

List<String> list = new ArrayList<>();
// ...
List<String> loaded = (LinkedList<String>) ois.readObject(); // ClassCastException!

建議: 反序列化時要用與序列化相同的具體型別,或在不在意具體實作時使用介面(List)。

使用程式庫(例如,Gson、Jackson)

JSON 程式庫(例如 GsonJackson)能序列化/反序列化含泛型的集合,但由於型別擦除,反序列化時需要明確指定型別。以下是 Gson 的範例:

Type type = new com.google.gson.reflect.TypeToken<List<User>>(){}.getType();
List<User> users = gson.fromJson(json, type);

8. Map 與 Set 中的泛型與序列化

上述規則同樣適用於其他含泛型的集合:

  • 序列化 Map<String, Integer> 時,不會保存鍵和值的型別資訊。
  • 反序列化時必須轉成需要的型別,並留意實際內容。

範例:序列化 Map

Map<String, Integer> scores = new HashMap<>();
scores.put("Vasya", 90);
scores.put("Petya", 85);

// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("scores.ser"))) {
    oos.writeObject(scores);
}

// 反序列化
Map<String, Integer> loadedScores;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("scores.ser"))) {
    loadedScores = (Map<String, Integer>) ois.readObject();
}

System.out.println(loadedScores); // {Vasya=90, Petya=85}

9. 序列化泛型集合的常見錯誤

錯誤 1:反序列化時出現 ClassCastException。 當你把反序列化的集合當作 List<String> 使用,而其中包含其他型別的物件時,就會在執行期得到 ClassCastException。請務必檢查集合內容!

錯誤 2:因元素不可序列化導致 NotSerializableException。 只要集合中有任一元素未實作 Serializable,序列化就會以 NotSerializableException 失敗。請檢查所有可能出現在集合中的類別都可被序列化。

錯誤 3:泛型參數資訊遺失。 反序列化之後不要依賴泛型參數——執行期沒有這些資訊。如有疑慮,請進行明確的型別檢查。

錯誤 4:集合實作不相符。 序列化的是 ArrayList,反序列化時卻當作 LinkedList 使用——會造成轉型錯誤。請盡量用與序列化相同的型別進行反序列化。

錯誤 5:類別版本不相容。 如果序列化之後變更了集合元素類別的結構(例如新增欄位),反序列化時可能會出錯。請使用 serialVersionUID 控制版本。

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