15.8.3 对象引用的序列化
15.8.3 对象引用的序列化
可序列化类 的成员变量 的类型必须可序列化
前面介绍的Person类的两个成员变量分别是String类型和int类型,如果某个类的成员变量的类型不是基本类型或String类型,而是另一个引用类型,那么这个引用类必须是可序列化的,否则拥有该类型成员变量的类也是不可序列化的。
程序
如下Teacher类持有一个Person类的引用,只有Person类是可序列化的,Teacher类才是可序列化的。如果Person类不可序列化,不管Teacher类是否实现Sterilizable或Externalizable接口,Teacher类都是不可序列化的。
这是因为当程序序列化一个Teacher对象时,如果该Teacher对象持有一个Person对象的引用为了在反序列化时可以正常恢复该Teacher对象,程序会顺带将该Person对象也进行序列化,所以Person类也必须是可序列化的,否则Teacher类将不可序列化。
1 | public class Teacher implements java.io.Serializable { |
一个对象可能被多次序列化的情况
现在假设有如下一种特殊情形:
程序中有两个Teacher对象,它们的Student实例变量都引用到同个Person对象,而且该Person对象还有一个引用变量引用它。如下代码所示。
1 | Person per= new Person("孙悟空",500); |
上面代码创建了两个Teacher对象和一个Person对象,这三个对象在内存中的存储示意图如图15.13所示。
这里产生了一个问题:
- 如果先序列化
t1对象,则系统将该t1对象所引用的Person对象一起序列化;- 如果程序再序列化
t2对象,系统将一样会序列化该t2对象,并且将再次序列化该t2对象所引用的Person对象;- 如果程序再显式序列化
per对象,系统将再次序列化该Person对象。- 这个过程似乎会向输出流中输出三个
Person对象
- 这个过程似乎会向输出流中输出三个
- 如果程序再显式序列化
- 如果程序再序列化
如果系统向输出流中写入了三个Person对象,那么后果是当程序从输入流中反序列化这些对象时,将会得到三个Person对象,从而引起t1和t2所引用的Person对象不是同一个对象,这显然与图5.13所示的效果不一致——这也就违背了Java序列化机制的初衷。
Java序列化机制
所以,Java序列化机制采用了一种特殊的序列化算法,其算法内容如下。
- 所有保存到磁盘中的对象都有一个序列化编号。
- 当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,
- 只有该对象从未(在本次虚拟机中)被序列化过,系统才会将该对象转换成字节序列并输出。
- 如果某个对象已经序列化过,程序将只是直接输出一个序列化编号,而不是再次重新序列化该对象。
根据上面的序列化算法,可以得到一个结论:
当第二次、第三次序列化Person对象时,程序不会再次将Person对象转换成字节序列并输出,而是仅仅输出一个序列化编号。假设有如下顺序的序列化代码
1 | oos.writeObject(t1); |
上面代码依次序列化了t1、t2和per对象,序列化后磁盘文件的存储示意图如图15.14所示。
通过图15.14可以很好地理解Java序列化的底层机制,通过该机制不难看出,当多次调用writeObject()方法输出同一个对象时,只有第一次调用writeObject()方法时才会将该对象转换成字节序列并输出。
程序 多次写入一个对象
下面程序序列化了两个Teacher对象,两个Teacher对象都持有一个引用到同一个Person对象的引用,而且程序两次调用writeObject()方法输出同一个Teacher对象。
1 | import java.io.*; |
上面程序中4次调用了writeObject()方法来输出对象,实际上只序列化了三个对象,而且序列的两个Teacher对象的student引用实际是同一个Person对象。
程序 读取被写入多次的对象
下面程序读取序列化文件中的对象即可证明这一点。
1 | import java.io.*; |
运行效果:
1 | t1的student引用和p是否相同:true |
上面程序中依次读取了序列化文件中的4个Java对象,但通过后面比较判断,不难发现t2和t3是同一个Java对象,t1的student引用的、t2的student引用的和p引用变量引用的也是同个Java对象——这证明了图15.14所示的序列化机制。
序列化可变对象引起的问题
由于Java序列化机制使然:如果多次序列化同一个Java对象时,只有第一次序列化时才会把该Java对象转换成字节序列并输出,这样可能引起一个潜在的问题——当程序序列化一个可变对象时,只有第一次使用writeObject()方法输出时才会将该对象转换成字节序列并输出,当程序再次调用writeObject()方法时,程序只是输出前面的序列化编号,即使后面该对象的实例变量值已被改变,改变的实例变量值也不会被输出。
如下程序所示。
1 | import java.io.*; |
程序中先使用writeObject()方法写入了一个Person对象,接着程序改变了Person对象的name实例变量值,然后程序再次输出Person对象,但这次的输出已经不会将Person对象转换成字节序列并输出了,而是仅仅输出了一个序列化编号
程序中代码1,代码2两次调用readObject()方法读取了序列化文件中的Java对象,比较两次读取的Java对象将完全相同,程序输岀第二次读取的Person对象的name实例变量的值依然是“孙悟空”,这表明改变后的Person对象并没有被写入,这与Java序列化机制相符。
只有第一次调用wirteObject方法时才会将对象转换成字节序列
当使用Java序列化机制序列化可变对象时一定要注意,只有第一次调用wirteObject方法来输出对象时才会将对象转换成字节序列,并写入到ObjectOutputStream,在后面程序中即使该对象的实例变量发生了改变,再次调用writeObjecto方法输出该对象时,改变后的实例变量也不会被输出。