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
一切正常:巢狀物件 Address 與 Person 一起被序列化並還原。
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;
}
即使建立了互相參照的 A 與 B,序列化也不會造成 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 標準函式庫的集合類別(ArrayList、HashMap 等)都實作了 Serializable,因此可以「開箱即用」。
8. 序列化巢狀物件的常見錯誤
錯誤 1:某個巢狀物件未實作 Serializable。 你遺漏了在某個巢狀類別上加上 implements Serializable。結果是第一次序列化就拋出 NotSerializableException。請檢查序列化鏈上的所有類別都支援此介面。
錯誤 2:不可序列化的欄位沒有標記為 transient。 如果有不該序列化的欄位(例如串流、資料庫連線、暫時性物件),卻沒有將其標記為 transient,序列化就會失敗拋錯。別忘了 transient!
錯誤 3:巢狀類別的 serialVersionUID 不一致。 如果你在巢狀類別中明確宣告了 serialVersionUID 並改變了其結構,記得同步更新這個識別碼——否則在反序列化時可能會出錯。
錯誤 4:可變的巢狀物件。 如果序列化了之後仍會變動的集合或物件(例如朋友清單),反序列化後所得到的是序列化當下的「快照」。原始物件後續的變更不會反映到反序列化後的物件中。
錯誤 5:序列化龐大的物件圖。 如果結構非常複雜且含有大量巢狀物件,序列化可能會耗費大量時間與記憶體。有時候只序列化關鍵資料,而非整個結構,會更合適。
GO TO FULL VERSION