CodeGym /課程 /JAVA 25 SELF /巢狀物件的序列化:特點

巢狀物件的序列化:特點

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

1. 前言:含引用欄位的物件序列化

在實際應用中,很少遇到完全「扁平」的類別。通常一個物件會包含其他物件,而那些物件又可能再包含更多東西。這稱為物件的組合(或巢狀)。例如:

public class Address {
    String city;
    String street;
}

public class Person {
    String name;
    int age;
    Address address; // 嵌套物件!
}

當我們序列化這樣的物件時,就會產生問題:欄位 address 該怎麼處理?Java 是否應該跟著 Person 一起序列化它?如果 Address 裡面還有其他物件呢?幸好(或不幸——視情況而定),Java 會預設以遞迴方式序列化所有巢狀物件,前提是它們也實作了介面 Serializable

在 Java 中,序列化一律是深度序列化(deep serialization)。這表示不僅序列化物件本身,還會序列化它透過其(非 transient)欄位所引用的所有物件,依此類推——一路「向下」。

流程視覺化

graph TD
    A[Person] --> B[Address]
    B --> C[CityInfo]
    A --> D[Pet]

總之,只要決定序列化 Person,Java 就會一併序列化 Address、以及 Address 內的所有東西,等等。

2. 巢狀物件的要求:必須是 Serializable!

你可能已經注意到一個重要的細節:所有要被序列化的巢狀物件,也都必須實作介面 Serializable

只要有任一引用欄位指向未實作 Serializable 的物件,嘗試序列化時就會拋出例外 java.io.NotSerializableException

讓我們看看幾個深度序列化的例子。

範例:一切正常

import java.io.Serializable;

public class Address implements Serializable {
    String city;
    String street;
}

public class Person implements Serializable {
    String name;
    int age;
    Address address;
}

兩個類別都實作了 Serializable。一切正常,序列化成功。

範例:錯誤!

public class Address { // 未實作 Serializable!
    String city;
    String street;
}

public class Person implements Serializable {
    String name;
    int age;
    Address address;
}

如可所見,這裡的 Address 沒有實作介面 Serializable。因此在序列化 Person 時會拋出例外 NotSerializableException

3. 範例:含巢狀物件的序列化與反序列化

來看看實務上如何操作。上一章節我們用過「聯絡人管理器」應用,現在給每位使用者都加上一個地址。

import java.io.*;

class Address implements Serializable {
    String city;
    String street;

    Address(String city, String street) {
        this.city = city;
        this.street = street;
    }
}

class Person implements Serializable {
    String name;
    int age;
    Address address;

    Person(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }
}

public class SerializationDemo {
    public static void main(String[] args) throws Exception {
        Person p = new Person("伊萬", 30, new Address("布拉格", "斯拉文斯卡, 1"));
        // 序列化
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
        out.writeObject(p);
        out.close();

        // 反序列化
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
        Person restored = (Person) in.readObject();
        in.close();

        System.out.println(restored.name + ", " + restored.age + ", " +
                           restored.address.city + ", " + restored.address.street);
    }
}

結果:

伊萬, 30, 布拉格, 斯拉文斯卡, 1

一切正常:巢狀物件 AddressPerson 一起被序列化並還原。

4. 如果巢狀物件不可序列化怎麼辦?

只要有任一引用欄位指向未實作 Serializable 的物件,Java 會在嘗試序列化時就拋出例外。

錯誤示範

class Address { // 未實作 Serializable!
    String city;
    String street;
}

class Person implements Serializable {
    String name;
    Address address;
}

public class Test {
    public static void main(String[] args) throws Exception {
        Person p = new Person();
        p.name = "Petya";
        p.address = new Address();
        p.address.city = "Derry";
        p.address.street = "Vyazov";
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
        out.writeObject(p); // <-- 會拋出例外!
        out.close();
    }
}

錯誤:

java.io.NotSerializableException: Address

5. 對巢狀物件使用 transient

如果你有一個欄位指向不該被序列化的物件(例如快取、資料庫連線、暫時性物件),就把該欄位標記為 transient。如此一來 Java 在序列化時會略過它。

含 transient 的範例

class Address { // 未實作 Serializable
    String city;
    String street;
}

class Person implements Serializable {
    String name;
    transient Address address; // transient!

    Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Address addr = new Address();
        addr.city = "Los Santos";
        addr.street = "Mulholland Drive";
        Person p = new Person("Sasha", addr);

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
        out.writeObject(p);
        out.close();

        // 反序列化
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
        Person restored = (Person) in.readObject();
        in.close();

        System.out.println(restored.name); // "Sasha"
        System.out.println(restored.address); // null!
    }
}

結果:
欄位 address 在反序列化後為 null,因為它是 transient

6. 深度巢狀與遞迴

序列化是以遞迴方式進行:如果 Person 有欄位 Address,而 Address 又有欄位 CityInfo,等等,序列化器會一路往下「潛水」,直到遇到不可序列化的東西,或是記憶體用完為止(玩笑話,但也不完全是)。

重點:循環參照

Java 的序列化器能處理循環參照。如果某個物件有指向另一個物件的參照,而後者又指回來,序列化器不會陷入無限循環,而是會妥善保存結構。

class A implements Serializable {
    B b;
}
class B implements Serializable {
    A a;
}

即使建立了互相參照的 AB,序列化也不會造成 StackOverflowError——Java 會記住已序列化過的物件。

7. 範例:序列化含巢狀清單的物件

物件內常包含其他物件的集合。例如,使用者可能有一個朋友清單:

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

class Person implements Serializable {
    String name;
    List<Person> friends;

    Person(String name) {
        this.name = name;
        this.friends = new ArrayList<>();
    }
}

public class FriendsSerialization {
    public static void main(String[] args) throws Exception {
        Person alice = new Person("Alice");
        Person bob = new Person("Bob");
        alice.friends.add(bob);

        // 序列化
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("friends.ser"));
        out.writeObject(alice);
        out.close();

        // 反序列化
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("friends.ser"));
        Person restored = (Person) in.readObject();
        in.close();

        System.out.println(restored.name); // Alice
        System.out.println(restored.friends.get(0).name); // Bob
    }
}

重點:Java 標準函式庫的集合類別(ArrayListHashMap 等)都實作了 Serializable,因此可以「開箱即用」。

8. 序列化巢狀物件的常見錯誤

錯誤 1:某個巢狀物件未實作 Serializable。 你遺漏了在某個巢狀類別上加上 implements Serializable。結果是第一次序列化就拋出 NotSerializableException。請檢查序列化鏈上的所有類別都支援此介面。

錯誤 2:不可序列化的欄位沒有標記為 transient。 如果有不該序列化的欄位(例如串流、資料庫連線、暫時性物件),卻沒有將其標記為 transient,序列化就會失敗拋錯。別忘了 transient

錯誤 3:巢狀類別的 serialVersionUID 不一致。 如果你在巢狀類別中明確宣告了 serialVersionUID 並改變了其結構,記得同步更新這個識別碼——否則在反序列化時可能會出錯。

錯誤 4:可變的巢狀物件。 如果序列化了之後仍會變動的集合或物件(例如朋友清單),反序列化後所得到的是序列化當下的「快照」。原始物件後續的變更不會反映到反序列化後的物件中。

錯誤 5:序列化龐大的物件圖。 如果結構非常複雜且含有大量巢狀物件,序列化可能會耗費大量時間與記憶體。有時候只序列化關鍵資料,而非整個結構,會更合適。

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