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
方法输出该对象时,改变后的实例变量也不会被输出。