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,而集合里既有 Cat,也有 Dog

示例:序列化包含子类的集合

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("Kitty"));
        animals.add(new Dog("Spot"));

        // 序列化
        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());
        }
    }
}

结果:

Kitty 说: Meow!
Spot 说: 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("Sci-Fi", Arrays.asList(
                new PaperBook("Dune", 800),
                new EBook("火星救援", "epub")
        ));
        library.catalog.put("经典", Collections.singletonList(
                new PaperBook("War and Peace", 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() + ")");
            }
        }
    }
}

输出:

类型: Sci-Fi
 - Dune (PaperBook)
 - 火星救援 (EBook)
类型: 经典
 - War and Peace (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