1. 集合的序列化
好消息:幾乎所有 Java 標準集合(例如 ArrayList、HashSet、HashMap 等)都已經實作了 Serializable。這意味著你可以直接開箱序列化它們,無須大費周章。
範例:
import java.io.Serializable;
import java.util.ArrayList;
public class MyList extends ArrayList<String> implements Serializable {
// 甚至什麼都不用寫 — ArrayList 已經是 Serializable!
}
實務上你通常會序列化標準集合,而且一切都能順利運作,省去多餘的麻煩。
如何序列化集合?
序列化集合的方式與序列化任何其他物件相同:
- 建立集合。
- 透過 ObjectOutputStream 將其寫入檔案。
- 透過 ObjectInputStream 讀回。
範例 1:序列化 ArrayList<String>
import java.io.*;
import java.util.*;
public class SerializeListDemo {
public static void main(String[] args) throws Exception {
// 1. 建立字串清單
List<String> books = new ArrayList<>();
books.add("麥田捕手");
books.add("遠大前程");
books.add("神曲");
// 2. 將清單儲存到檔案
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("books.ser"));
out.writeObject(books);
out.close();
// 3. 從檔案讀回清單
ObjectInputStream in = new ObjectInputStream(new FileInputStream("books.ser"));
List<String> loadedBooks = (List<String>) in.readObject();
in.close();
// 4. 檢查結果
System.out.println("反序列化後的書籍清單:");
for (String book : loadedBooks) {
System.out.println("- " + book);
}
}
}
輸出:
反序列化後的書籍清單:
- 麥田捕手
- 遠大前程
- 神曲
範例 2:序列化 HashSet<Integer>
Set<Integer> numbers = new HashSet<>(Arrays.asList(10, 20, 30, 40));
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("numbers.ser"));
out.writeObject(numbers);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("numbers.ser"));
Set<Integer> loadedNumbers = (Set<Integer>) in.readObject();
in.close();
System.out.println("反序列化後的集合: " + loadedNumbers);
範例 3:序列化 HashMap<String, Integer>
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 100);
scores.put("Bob", 80);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("scores.ser"));
out.writeObject(scores);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("scores.ser"));
Map<String, Integer> loadedScores = (Map<String, Integer>) in.readObject();
in.close();
System.out.println("反序列化後的 Map: " + loadedScores);
重點
順序是否會保留?
- 對於 List(例如 ArrayList),元素順序一定會保留。
- 對於 Set(HashSet),順序不保證(序列化前後皆然)。
- 對於 Map(HashMap),鍵值對的順序不保證(若需要順序 — 請使用 LinkedHashMap)。
空集合 可以正常序列化與反序列化。反序列化後你會得到空的清單/集合/映射。
巢狀集合(例如 List<List<String>>)只要所有巢狀元素可被序列化,就能正確序列化。
2. 集合元素的要求
這裡常常潛藏著第一個大陷阱!
集合中的所有元素也必須是可序列化的。
如果集合中至少有一個元素沒有實作 Serializable,則序列化會以 NotSerializableException 失敗。而且如果集合很大、錯誤出在深處,找出元兇會變成一場大冒險。
範例:序列化包含不可序列化物件的集合
import java.io.*;
import java.util.*;
class NotSerializableClass {
int value;
public NotSerializableClass(int value) {
this.value = value;
}
}
public class NotSerializableDemo {
public static void main(String[] args) throws Exception {
List<Object> list = new ArrayList<>();
list.add("Hello");
list.add(new NotSerializableClass(123)); // <- 糟了!
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("badlist.ser"));
try {
out.writeObject(list); // 這裡會丟出例外!
} catch (NotSerializableException e) {
System.out.println("序列化錯誤: " + e);
}
out.close();
}
}
結果:
序列化錯誤: java.io.NotSerializableException: NotSerializableClass
怎麼辦?
解法其實很簡單:集合中的所有元素要嘛是標準型別,例如 String 或 Integer,要嘛是你自己的類別,且這些類別有實作 Serializable。如果某個類別不可序列化,只要在類別上加入 implements Serializable 就能解決問題。
3. 不同集合的序列化特性
元素順序是否會保留?
- List:會,元素順序會被保留。
- Set:取決於實作。在 HashSet 中不保證順序,而在 LinkedHashSet 中則會保留。
- Map:在 HashMap 中不保證順序,在 LinkedHashMap 中則會保留插入順序。
示範:
List<String> list = Arrays.asList("A", "B", "C");
Set<String> set = new HashSet<>(list);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("coll.ser"));
out.writeObject(list);
out.writeObject(set);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("coll.ser"));
List<String> loadedList = (List<String>) in.readObject();
Set<String> loadedSet = (Set<String>) in.readObject();
in.close();
System.out.println("List: " + loadedList); // 永遠是 [A, B, C]
System.out.println("Set: " + loadedSet); // 可能是 [A, C, B] 等等
序列化空集合
空集合能夠順利序列化。反序列化後你會得到對應型別的空物件。
序列化巢狀集合
可以序列化包含其他集合的集合:
List<List<String>> table = new ArrayList<>();
table.add(Arrays.asList("row1-col1", "row1-col2"));
table.add(Arrays.asList("row2-col1", "row2-col2"));
// 序列化
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("table.ser"));
out.writeObject(table);
out.close();
// 反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("table.ser"));
List<List<String>> loadedTable = (List<List<String>>) in.readObject();
in.close();
System.out.println(loadedTable);
4. 實作範例:序列化自訂類別物件的集合
我們一起寫個「虛擬圖書館」的小應用,來序列化書籍清單。類別 Book 必須是可序列化的!
import java.io.*;
import java.util.*;
class Book implements Serializable {
private static final long serialVersionUID = 1L; // 用於類別版本相容性
String title;
String author;
int year;
public Book(String title, String author, int year) {
this.title = title;
this.author = author;
this.year = year;
}
@Override
public String toString() {
return title + " (" + author + ", " + year + ")";
}
}
public class LibraryApp {
public static void main(String[] args) throws Exception {
List<Book> books = new ArrayList<>();
books.add(new Book("麥田捕手", "J. D. 薩林傑", 1951));
books.add(new Book("遠大前程", "查爾斯·狄更斯", 1861));
books.add(new Book("神曲", "但丁·阿利吉耶里", 1320));
// 將清單儲存到檔案
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("mylibrary.ser"));
out.writeObject(books);
out.close();
// 將清單讀回
ObjectInputStream in = new ObjectInputStream(new FileInputStream("mylibrary.ser"));
List<Book> loadedBooks = (List<Book>) in.readObject();
in.close();
System.out.println("檔案中的書籍:");
for (Book b : loadedBooks) {
System.out.println("- " + b);
}
}
}
5. 序列化集合的常見錯誤
錯誤 1:集合元素上的 NotSerializableException。 只要集合裡有一個元素不可序列化,就會因 NotSerializableException 而失敗。例如你不小心把沒有實作 Serializable 的物件加進集合,或忘了在自己的類別上加上該介面。
錯誤 2:在序列化與反序列化之間修改了類別。 如果你序列化了一組物件,之後又修改了類別結構(例如新增或刪除欄位),反序列化時可能拋出 InvalidClassException。為避免此問題,請在類別中使用 serialVersionUID 欄位。
錯誤 3:序列化包含 transient 或 static 欄位的集合。 標記為 transient 或 static 的欄位不會被序列化。如果你的物件依賴這些欄位,反序列化後它們會是預設值(例如 null 或 0)。
錯誤 4:序列化包含巢狀不可序列化物件的集合。 如果集合中存在巢狀集合或未實作 Serializable 的物件,錯誤會發生在最深層 — 並不總是容易判斷問題點在哪。請檢查所有巢狀層級!
錯誤 5:面對大型集合的效能問題。 如果集合非常龐大,序列化可能會耗費大量時間與磁碟空間。此時應考慮串流式序列化,或將集合分塊處理。
GO TO FULL VERSION