1. 引言
在 Java(以及 OOP)中,每个对象都有身份,它不仅是字段值的集合,更是内存中“它是谁”。两个对象可以“等价”(例如,a.equals(b) 返回 true),但身份不同(a != b)。 所谓身份,是指两个变量指向内存中的同一个对象(a == b)。
为什么这对序列化很重要?
当你序列化(保存)一个对象图(例如树、列表,或者两个对象引用同一个嵌套对象)时,重要的不仅是字段值,还有对象之间的引用结构。
如果对象图中存在重复引用(多个对象引用同一个嵌套对象),甚至存在循环引用(A → B → A),那么在反序列化后,这些关系应保持不变。
例如:你有两个对象 A 和 B,它们都引用同一个对象 C。序列化并反序列化之后,应当满足:a.c == b.c(内存中的同一对象)。如果序列化只是“复制”对象,那么恢复后会得到两个不同的 C,这将改变原有的对象图。
2. ObjectOutputStream 如何处理身份问题
在 Java 中,二进制序列化使用一对类:ObjectOutputStream(写出)和 ObjectInputStream(读入)。
ObjectOutputStream 并不是简单的“把对象写入文件”。它很智能:
- 在一次写出会话中,跟踪所有已经被序列化的对象。
- 如果遇到对已序列化对象的引用,它不会重复写入对象本体,而是写入特殊的“重复引用”(reference handle)。
- 在反序列化时,ObjectInputStream 会恢复结构,使得重复引用指向内存中的同一对象。
这对循环引用同样有效!也就是说,如果你有 A → B → A,序列化与反序列化不会陷入死循环或失败:引用会被正确恢复。
它是如何实现的?
在 ObjectOutputStream 内部使用了一张特殊的表(identity map),用来保存已序列化的对象。遇到新对象时,将其加入表中并完整序列化;遇到已在表中的对象时,只向流中写入“重复引用”(handle)。
3. 示例演示
示例 1:具有循环引用的图(A → B,B → A)
import java.io.*;
class Node implements Serializable {
String name;
Node next;
Node(String name) {
this.name = name;
}
}
public class CyclicSerializationDemo {
public static void main(String[] args) throws Exception {
Node a = new Node("A");
Node b = new Node("B");
a.next = b;
b.next = a; // 循环!
// 序列化
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cyclic.dat"))) {
out.writeObject(a);
}
// 反序列化
Node a2;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("cyclic.dat"))) {
a2 = (Node) in.readObject();
}
System.out.println("a2.name = " + a2.name); // A
System.out.println("a2.next.name = " + a2.next.name); // B
System.out.println("a2.next.next == a2: " + (a2.next.next == a2)); // true!
}
}
输出:
a2.name = A
a2.next.name = B
a2.next.next == a2: true
说明:
循环被保留下来!反序列化后,a2.next.next 再次指向 a2。
示例 2:重复引用(A → C,B → C)
import java.io.*;
class Wrapper implements Serializable {
String name;
Object ref;
Wrapper(String name) { this.name = name; }
}
public class SharedReferenceDemo {
public static void main(String[] args) throws Exception {
Wrapper a = new Wrapper("A");
Wrapper b = new Wrapper("B");
Wrapper c = new Wrapper("C");
a.ref = c;
b.ref = c;
// 序列化
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("shared.dat"))) {
out.writeObject(new Wrapper[] {a, b});
}
// 反序列化
Wrapper[] arr;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("shared.dat"))) {
arr = (Wrapper[]) in.readObject();
}
Wrapper a2 = arr[0];
Wrapper b2 = arr[1];
System.out.println("a2.ref == b2.ref: " + (a2.ref == b2.ref)); // true!
System.out.println("a2.ref.name = " + ((Wrapper)a2.ref).name); // C
}
}
输出:
a2.ref == b2.ref: true
a2.ref.name = C
说明:
反序列化后,两条引用再次指向同一个对象。
4. 与 writeReplace 和 readResolve 的关系
在 Java 中,可以通过一些特殊方法来“介入”(反)序列化过程:
writeReplace() —— 在对象被序列化之前调用。可以返回另一个对象代替原对象进行序列化。
readResolve() —— 在对象反序列化之后调用。可以返回另一个对象来替换刚刚创建的对象。
对身份的影响:
- 如果使用了 writeReplace() 或 readResolve(),则这些方法返回的对象才会进入引用表。
- 这可能改变身份:如果 readResolve() 返回新的/不同的对象,重复引用可能会指向不同的实例。
- 务必小心:你既可能“破坏”身份,也可以有意维护它(经典示例——通过 readResolve() 的单例 Singleton,使反序列化后的所有引用都指向同一个单例)。
5. 实践:演示身份保持的代码
import java.io.*;
class Shared implements Serializable {
String value;
Shared(String value) { this.value = value; }
}
class Holder implements Serializable {
String name;
Shared shared;
Holder(String name, Shared shared) {
this.name = name;
this.shared = shared;
}
}
public class IdentityDemo {
public static void main(String[] args) throws Exception {
Shared c = new Shared("C");
Holder a = new Holder("A", c);
Holder b = new Holder("B", c);
// 序列化
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("identity.dat"))) {
out.writeObject(new Holder[] {a, b});
}
// 反序列化
Holder[] arr;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("identity.dat"))) {
arr = (Holder[]) in.readObject();
}
Holder a2 = arr[0];
Holder b2 = arr[1];
System.out.println("a2.shared == b2.shared: " + (a2.shared == b2.shared)); // true!
System.out.println("a2.shared.value = " + a2.shared.value); // C
}
}
输出:
a2.shared == b2.shared: true
a2.shared.value = C
说明:
对象 c 的身份被保留:反序列化后,两条引用都指向同一个实例。
6. 总结与常见错误
错误 1:将包含重复引用的对象分开序列化。 如果你为每个对象各用一个 ObjectOutputStream 实例来写,二者之间的引用关系将不会被保存——反序列化得到的实例会不同,即使它们在原始对象图中引用的是同一个对象。
错误 2:错误使用 writeReplace() 和 readResolve()。 这些方法会在(反)序列化过程中替换对象,从而改变其身份。如果不了解其机制,最终可能得到出乎意料的实例。
错误 3:共享的可变引用带来的意外影响。 如果多个对象引用同一个可变的嵌套对象(例如列表),反序列化后仍将如此。某处的修改会影响到其他所有引用处。
错误 4:期待得到“全新”的对象。 序列化不会创建“全新”的结构——它会按原样恢复引用关系。这在使用缓存或模板对象时可能出乎意料。
GO TO FULL VERSION