1. 集合中的集合序列化
在 Java 中,集合不僅可以包含簡單型別(例如 String),也可以包含其他集合或物件。這讓我們能建立複雜的結構:例如 Map<String, List<User>>,其中 User 是你自訂的類別。
範例:含巢狀 List 的 Map 序列化
來看一個小型社群網路的例子:每位使用者都有一份朋友清單。
import java.io.*;
import java.util.*;
class User implements Serializable {
private static final long serialVersionUID = 1L;
String name;
User(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" + "name='" + name + '\'' + '}';
}
}
public class SocialNetwork implements Serializable {
private static final long serialVersionUID = 1L;
Map<String, List<User>> friends = new HashMap<>();
public static void main(String[] args) throws IOException, ClassNotFoundException {
SocialNetwork network = new SocialNetwork();
network.friends.put("alice", Arrays.asList(new User("bob"), new User("carol")));
network.friends.put("bob", Collections.singletonList(new User("alice")));
// 序列化
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("network.ser"))) {
out.writeObject(network);
}
// 反序列化
SocialNetwork loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("network.ser"))) {
loaded = (SocialNetwork) in.readObject();
}
System.out.println("還原的網路:" + loaded.friends);
}
}
可以這麼說:所有使用到的型別——不論是 HashMap、ArrayList 還是 User——都實作了 Serializable 介面。序列化時,Java 會自動走訪所有巢狀的集合與物件,並一併寫出。因此反序列化後,你會得到完整還原的結構,包括所有巢狀清單。
輸出:
還原的網路:{alice=[User{name='bob'}, User{name='carol'}], bob=[User{name='alice'}]}
想要多深的巢狀都行
你可以建立任意多層的巢狀:List<List<User>>、Map<String, Map<Integer, List<User>>>——Java 不怕遞迴(當然要在合理範圍內)。
2. 階層式物件:含繼承層級的集合序列化
如果集合中包含具繼承關係的物件呢?例如,你有基底類別 Animal,而集合裡同時放了 Cat 與 Dog?
範例:包含子類的集合序列化
import java.io.*;
import java.util.*;
abstract class Animal implements Serializable {
private static final long serialVersionUID = 1L;
String name;
Animal(String name) {
this.name = name;
}
public abstract String speak();
}
class Cat extends Animal {
private static final long serialVersionUID = 1L;
Cat(String name) {
super(name);
}
@Override
public String speak() {
return "Meow!";
}
}
class Dog extends Animal {
private static final long serialVersionUID = 1L;
Dog(String name) {
super(name);
}
@Override
public String speak() {
return "Woof!";
}
}
public class Zoo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
List<Animal> animals = new ArrayList<>();
animals.add(new Cat("Murka"));
animals.add(new Dog("Sharik"));
// 序列化
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("zoo.ser"))) {
out.writeObject(animals);
}
// 反序列化
List<Animal> loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("zoo.ser"))) {
loaded = (List<Animal>) in.readObject();
}
for (Animal animal : loaded) {
System.out.println(animal.name + " 說:" + animal.speak());
}
}
}
結果:
Murka 說: Meow!
Sharik 說: Woof!
重點: Java 不僅會序列化基底類別的欄位,還會保存物件的實際型別。因此反序列化後,物件仍保有其「貓」或「狗」的本質,你可以安全地呼叫它們的方法。
3. 物件圖的序列化
現在該來看看真正的魔法——序列化物件圖,物件之間可以彼此參照,而不只是彼此巢狀。不過先來釐清什麼是物件圖。
什麼是物件圖?
物件圖是一種結構,其中物件透過欄位參照彼此連結。舉例來說,在家譜中,每個人都可能有指向父母、子女、兄弟姐妹的參照。
類比: 想像社群網路中的一群朋友:每位使用者都有朋友清單,而這些朋友也是使用者,他們也有自己的朋友,如此延伸下去。這就是物件圖。
範例:雙向連結串列的序列化
import java.io.*;
class Node implements Serializable {
private static final long serialVersionUID = 1L;
String value;
Node next;
Node prev;
Node(String value) {
this.value = value;
}
}
public class DoublyLinkedListDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 建立兩個相連的節點
Node first = new Node("A");
Node second = new Node("B");
first.next = second;
second.prev = first;
// 序列化
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("list.ser"))) {
out.writeObject(first);
}
// 反序列化
Node loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("list.ser"))) {
loaded = (Node) in.readObject();
}
System.out.println("第一個節點的值:" + loaded.value); // "A"
System.out.println("下一個:" + loaded.next.value); // "B"
System.out.println("下一個的前一個:" + loaded.next.prev.value); // "A"
}
}
請注意,實際只序列化了一個參照(first),但由於遞迴式的序列化,Java 會「走訪」所有相關物件。反序列化時參照結構會被完整還原:loaded.next.prev == loaded 會是 true!即使圖中有循環(例如節點彼此互相參照),Java 的標準序列化也能正確處理而不會陷入無限迴圈。
4. 巢狀與階層式集合:真實類別範例
模型:書籍目錄
假設有個 Book 類別,它可以是實體書或電子書(繼承)。還有一個 Library 類別,內含類型到書單的對應(Map<String, List<Book>>)。每個類型對應一個書籍清單。
import java.io.*;
import java.util.*;
abstract class Book implements Serializable {
private static final long serialVersionUID = 1L;
String title;
Book(String title) {
this.title = title;
}
}
class PaperBook extends Book {
private static final long serialVersionUID = 1L;
int pages;
PaperBook(String title, int pages) {
super(title);
this.pages = pages;
}
}
class EBook extends Book {
private static final long serialVersionUID = 1L;
String format;
EBook(String title, String format) {
super(title);
this.format = format;
}
}
class Library implements Serializable {
private static final long serialVersionUID = 1L;
Map<String, List<Book>> catalog = new HashMap<>();
}
public class CatalogDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Library library = new Library();
library.catalog.put("科幻", Arrays.asList(
new PaperBook("沙丘", 800),
new EBook("火星任務", "epub")
));
library.catalog.put("經典", Collections.singletonList(
new PaperBook("戰爭與和平", 1200)
));
// 序列化
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("library.ser"))) {
out.writeObject(library);
}
// 反序列化
Library loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("library.ser"))) {
loaded = (Library) in.readObject();
}
for (Map.Entry<String, List<Book>> entry : loaded.catalog.entrySet()) {
System.out.println("類型:" + entry.getKey());
for (Book book : entry.getValue()) {
System.out.println(" - " + book.title + " (" + book.getClass().getSimpleName() + ")");
}
}
}
}
輸出:
類型:科幻
- 沙丘 (PaperBook)
- 火星任務 (EBook)
類型:經典
- 戰爭與和平 (PaperBook)
總結:
- 巢狀集合(Map<String, List<Book>>)的序列化是開箱即用的。
- 物件的實際型別(PaperBook、EBook)會被保留。
- 反序列化後,結構會被完整還原。
5. 物件圖序列化:底層在做什麼?
當你序列化一個物件時,Java 會「走訪」它的所有欄位(以及欄位的欄位,依此類推),且每個物件只會被序列化一次。如果某個物件重複出現(例如在循環參照中),Java 會寫入一個特殊的引用,而不是再次序列化它。
視覺化(流程圖)
graph TD
A[物件 A] -- 欄位 --> B[物件 B]
B -- 欄位 --> C[物件 C]
C -- 欄位 --> A
Java 先序列化 A,再來是 B,接著是 C;當再度遇到 A 時,會寫入「指向已序列化之物件 A 的引用」。反序列化時會以相同的關係回復整個結構。
6. 物件圖序列化的特點
- 不怕循環:Java 的標準序列化支援循環參照,不會陷入無限迴圈,也不會造成 StackOverflow。
- 所有物件都必須可序列化:只要圖中的任何一個物件沒有實作 Serializable,序列化就會在該物件處失敗。
- 相同物件不會被重複:若同一個物件在圖中的多處被參照,反序列化後仍會是同一個物件(同一個參照)。
- 物件型別會被保留:即使集合宣告為 List<Animal>,反序列化後仍會得到實際類別的物件(Cat、Dog 等等)。
7. 序列化巢狀與階層式物件的常見錯誤
錯誤 №1:不是所有類別都可序列化。
很常見的問題是忘了在某個自訂類別上加入 implements Serializable,而該類別位於集合或巢狀物件之內。結果就是 NotSerializableException 與失望。請檢查整個巢狀鏈。
錯誤 №2:手動序列化時遺漏參照。
如果你自行實作 writeObject/readObject,卻忘了序列化某個欄位(例如指向父節點或巢狀集合的參照),反序列化後結構就會受損。務必測試還原結果。
錯誤 №3:對必要欄位使用 transient。
若把必要欄位標記為 transient,它將不會被寫入序列化資料流,還原後會是 null 或預設值,可能破壞物件圖的完整性。
錯誤 №4:序列化與反序列化之間變更了類別結構。
如果在序列化之後變更了類別結構(例如新增欄位),在反序列化時可能發生錯誤或資料遺失。請使用 serialVersionUID 並維持相容性。
錯誤 №5:序列化大型物件圖。
複雜且關聯緊密的結構可能產生非常大的檔案,且序列化/反序列化耗時。請留意大小,能拆分時就拆分。
錯誤 №6:序列化「原始」集合。
如果你宣告了沒有泛型參數的集合(例如只寫 List),反序列化後就得顯式轉型,容易導致 ClassCastException。請使用泛型並檢查型別。
GO TO FULL VERSION