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 = "彼佳";
        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;
}

如果创建相互引用的 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