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 = "彼佳";
p.address = new Address();
p.address.city = "德里";
p.address.street = "维亚佐夫";
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 = "洛圣都";
addr.street = "穆赫兰道";
Person p = new Person("萨沙", 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); // "萨沙"
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