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);
}
}
可以这样理解:所有使用到的类型——无论是 HashMap、ArrayList 还是 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>>)的序列化是“开箱即用”的。
- 对象的实际类型(PaperBook、EBook)会被保留。
- 反序列化后结构将被完整恢复。
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>,反序列化后你得到的将是它们的实际类(Cat、Dog 等)。
7. 序列化嵌套与层级对象时的常见错误
错误 1:不是所有类都可序列化。
人们常常忘了在某个自定义类上添加 implements Serializable,而这个类位于集合或嵌套对象之中。结果就是抛出 NotSerializableException,非常扫兴。请检查整条嵌套链!
错误 2:手写序列化时丢失引用。
如果你自行实现 writeObject/readObject,却忘记序列化某个字段(例如指向父对象或嵌套集合的引用),反序列化后结构就会被破坏。务必测试恢复结果。
错误 3:对需要的字段使用 transient。
如果把需要的字段标记为 transient,它不会写入序列化流,恢复后将为 null 或默认值。这可能破坏对象图的一致性。
错误 4:序列化与反序列化之间修改了类结构。
如果在对象序列化之后修改了类结构(例如新增字段),反序列化时可能出现错误或数据丢失。请使用 serialVersionUID 并保持兼容性。
错误 5:序列化大型对象图。
复杂的互相关联结构可能导致序列化/反序列化耗时很长且文件很大。关注体积,并尽可能拆分。
错误 6:序列化“裸”集合。
如果你声明了没有泛型参数的集合(例如仅仅是 List),反序列化后需要显式进行类型转换,这很容易引发 ClassCastException。请使用泛型并检查类型。
GO TO FULL VERSION