CodeGym /課程 /JAVA 25 SELF /巢狀與階層式物件:物件圖的序列化

巢狀與階層式物件:物件圖的序列化

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

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);
    }
}

可以這麼說:所有使用到的型別——不論是 HashMapArrayList 還是 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,而集合裡同時放了 CatDog

範例:包含子類的集合序列化

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>>)的序列化是開箱即用的。
  • 物件的實際型別(PaperBookEBook)會被保留。
  • 反序列化後,結構會被完整還原。

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>,反序列化後仍會得到實際類別的物件(CatDog 等等)。

7. 序列化巢狀與階層式物件的常見錯誤

錯誤 №1:不是所有類別都可序列化。
很常見的問題是忘了在某個自訂類別上加入 implements Serializable,而該類別位於集合或巢狀物件之內。結果就是 NotSerializableException 與失望。請檢查整個巢狀鏈。

錯誤 №2:手動序列化時遺漏參照。
如果你自行實作 writeObject/readObject,卻忘了序列化某個欄位(例如指向父節點或巢狀集合的參照),反序列化後結構就會受損。務必測試還原結果。

錯誤 №3:對必要欄位使用 transient
若把必要欄位標記為 transient,它將不會被寫入序列化資料流,還原後會是 null 或預設值,可能破壞物件圖的完整性。

錯誤 №4:序列化與反序列化之間變更了類別結構。
如果在序列化之後變更了類別結構(例如新增欄位),在反序列化時可能發生錯誤或資料遺失。請使用 serialVersionUID 並維持相容性。

錯誤 №5:序列化大型物件圖。
複雜且關聯緊密的結構可能產生非常大的檔案,且序列化/反序列化耗時。請留意大小,能拆分時就拆分。

錯誤 №6:序列化「原始」集合。
如果你宣告了沒有泛型參數的集合(例如只寫 List),反序列化後就得顯式轉型,容易導致 ClassCastException。請使用泛型並檢查型別。

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