15.8.3 对象引用的序列化

15.8.3 对象引用的序列化

可序列化类 的成员变量 的类型必须可序列化

前面介绍的Person类的两个成员变量分别是String类型和int类型,如果某个类的成员变量的类型不是基本类型或String类型,而是另一个引用类型,那么这个引用类必须是可序列化的,否则拥有该类型成员变量的类也是不可序列化的

程序

如下Teacher类持有一个Person类的引用,只有Person类是可序列化的,Teacher类才是可序列化的。如果Person类不可序列化,不管Teacher类是否实现SterilizableExternalizable接口,Teacher类都是不可序列化的。
这是因为当程序序列化一个Teacher对象时,如果该Teacher对象持有一个Person对象的引用为了在反序列化时可以正常恢复该Teacher对象,程序会顺带将该Person对象也进行序列化,所以Person类也必须是可序列化的,否则Teacher类将不可序列化。

1
2
3
4
5
6
7
8
9
10
public class Teacher implements java.io.Serializable {
private static final long serialVersionUID = -1184339136873829552L;
private String name;
private Person student;
public Teacher(String name, Person student) {
this.name = name;
this.student = student;
}
// 此处省略了name和student的setter和getter方法
}

一个对象可能被多次序列化的情况

现在假设有如下一种特殊情形:
程序中有两个Teacher对象,它们的Student实例变量都引用到同个Person对象,而且该Person对象还有一个引用变量引用它。如下代码所示。

1
2
3
Person per= new Person("孙悟空",500);
Teacher t1= new Teacher("唐僧",per);
Teacher t2= new Teacher("菩提祖师",per);

上面代码创建了两个Teacher对象和一个Person对象,这三个对象在内存中的存储示意图如图15.13所示。
这里有一张图片

这里产生了一个问题:

  • 如果先序列化t1对象,则系统将该t1对象所引用的Person对象一起序列化;
    • 如果程序再序列化t2对象,系统将一样会序列化该t2对象,并且将再次序列化该t2对象所引用的Person对象;
      • 如果程序再显式序列化per对象,系统将再次序列化该Person对象。
        • 这个过程似乎会向输出流中输出三个Person对象

如果系统向输出流中写入了三个Person对象,那么后果是当程序从输入流中反序列化这些对象时,将会得到三个Person对象,从而引起t1t2所引用的Person对象不是同一个对象,这显然与图5.13所示的效果不一致——这也就违背了Java序列化机制的初衷。

Java序列化机制

所以,Java序列化机制采用了一种特殊的序列化算法,其算法内容如下。

  • 所有保存到磁盘中的对象都有一个序列化编号。
  • 当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,
    • 只有该对象从未(在本次虚拟机中)被序列化过,系统才会将该对象转换成字节序列并输出
    • 如果某个对象已经序列化过,程序将只是直接输出一个序列化编号,而不是再次重新序列化该对象。

根据上面的序列化算法,可以得到一个结论:
当第二次、第三次序列化Person对象时,程序不会再次将Person对象转换成字节序列并输出,而是仅仅输出一个序列化编号。假设有如下顺序的序列化代码

1
2
3
oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(per);

上面代码依次序列化了t1t2per对象,序列化后磁盘文件的存储示意图如图15.14所示。
这里有一张图片
通过图15.14可以很好地理解Java序列化的底层机制,通过该机制不难看出,当多次调用writeObject()方法输出同一个对象时,只有第一次调用writeObject()方法时才会将该对象转换成字节序列并输出

程序 多次写入一个对象

下面程序序列化了两个Teacher对象,两个Teacher对象都持有一个引用到同一个Person对象的引用,而且程序两次调用writeObject()方法输出同一个Teacher对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.*;

public class WriteTeacher {
public static void main(String[] args) {
try (
// 创建一个ObjectOutputStream输出流
ObjectOutputStream oos =
new ObjectOutputStream(new FileOutputStream("teacher.txt"))) {
Person per = new Person("孙悟空", 500);
Teacher t1 = new Teacher("唐僧", per);
Teacher t2 = new Teacher("菩提祖师", per);
// 依次将四个对象写入输出流
oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(per);
oos.writeObject(t2);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}

上面程序中4次调用了writeObject()方法来输出对象,实际上只序列化了三个对象,而且序列的两个Teacher对象的student引用实际是同一个Person对象。

程序 读取被写入多次的对象

下面程序读取序列化文件中的对象即可证明这一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.*;

public class ReadTeacher {
public static void main(String[] args) {
try (
// 创建一个ObjectInputStream输出流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("teacher.txt"))) {
// 依次读取ObjectInputStream输入流中的四个对象
Teacher t1 = (Teacher) ois.readObject();
Teacher t2 = (Teacher) ois.readObject();
Person p = (Person) ois.readObject();
Teacher t3 = (Teacher) ois.readObject();
// 输出true
System.out.println("t1的student引用和p是否相同:" + (t1.getStudent() == p));
// 输出true
System.out.println("t2的student引用和p是否相同:" + (t2.getStudent() == p));
// 输出true
System.out.println("t2和t3是否是同一个对象:" + (t2 == t3));
} catch (Exception ex) {
ex.printStackTrace();
}
}
}

运行效果:

1
2
3
t1的student引用和p是否相同:true
t2的student引用和p是否相同:true
t2和t3是否是同一个对象:true

上面程序中依次读取了序列化文件中的4个Java对象,但通过后面比较判断,不难发现t2t3是同一个Java对象,t1student引用的、t2student引用的和p引用变量引用的也是同个Java对象——这证明了图15.14所示的序列化机制。

序列化可变对象引起的问题

由于Java序列化机制使然:如果多次序列化同一个Java对象时,只有第一次序列化时才会把该Java对象转换成字节序列并输出,这样可能引起一个潜在的问题——当程序序列化一个可变对象时,只有第一次使用writeObject()方法输出时才会将该对象转换成字节序列并输出,当程序再次调用writeObject()方法时,程序只是输出前面的序列化编号,即使后面该对象的实例变量值已被改变,改变的实例变量值也不会被输出
如下程序所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.io.*;

public class SerializeMutable {
public static void main(String[] args) {

try (
// 创建一个ObjectOutputStream输入流
ObjectOutputStream oos =
new ObjectOutputStream(new FileOutputStream("mutable.txt"));
// 创建一个ObjectInputStream输入流
ObjectInputStream ois =
new ObjectInputStream(new FileInputStream("mutable.txt"))) {
Person per = new Person("孙悟空", 500);
// 系统会per对象转换字节序列并输出
oos.writeObject(per);
// 改变per对象的name实例变量
per.setName("猪八戒");
// 系统只是输出序列化编号,所以改变后的name不会被序列化
oos.writeObject(per);
Person p1 = (Person) ois.readObject(); // 代码1
Person p2 = (Person) ois.readObject(); // 代码2
// 下面输出true,即反序列化后p1等于p2
System.out.println(p1 == p2);
// 下面依然看到输出"孙悟空",即改变后的实例变量没有被序列化
System.out.println(p2.getName());
} catch (Exception ex) {
ex.printStackTrace();
}
}
}

程序中先使用writeObject()方法写入了一个Person对象,接着程序改变了Person对象的name实例变量值,然后程序再次输出Person对象,但这次的输出已经不会将Person对象转换成字节序列并输出了,而是仅仅输出了一个序列化编号
程序中代码1,代码2两次调用readObject()方法读取了序列化文件中的Java对象,比较两次读取的Java对象将完全相同,程序输岀第二次读取的Person对象的name实例变量的值依然是“孙悟空”,这表明改变后的Person对象并没有被写入,这与Java序列化机制相符。

只有第一次调用wirteObject方法时才会将对象转换成字节序列

当使用Java序列化机制序列化可变对象时一定要注意,只有第一次调用wirteObject方法来输出对象时才会将对象转换成字节序列,并写入到ObjectOutputStream,在后面程序中即使该对象的实例变量发生了改变,再次调用writeObjecto方法输出该对象时,改变后的实例变量也不会被输出。