2.4.2 理解对象序列化的文件格式
对象序列化是以特殊的文件格式存储对象数据的,当然,你不必了解文件中表示对象的确切字节序列,就可以使用writeObject/readObject方法。但是,我们发现研究这种数据格式对于洞察对象流化的处理过程非常有益。因为其细节显得有些专业,所以如果你对其实现不感兴趣,则可以跳过这一节。
每个文件都是以下面这两个字节的“魔幻数字”开始的
后面紧跟着对象序列化格式的版本号,目前是
(我们在本节中统一使用十六进制数字来表示字节。)然后,是它包含的对象序列,其顺序即它们存储的顺序。
字符串对象被存为
字符串中的Unicode字符被存储为修订过的UTF-8格式。
当存储一个对象时,这个对象所属的类也必须存储。这个类的描述包含
- 类名。
- 序列化的版本唯一的ID,它是数据域类型和方法签名的指纹。
- 描述序列化方法的标志集。
- 对数据域的描述。
指纹是通过对类、超类、接口、域类型和方法签名按照规范方式排序,然后将安全散列算法(SHA)应用于这些数据而获得的。
SHA是一种可以为较大的信息块提供指纹的快速算法,不论最初的数据块尺寸有多大,这种指纹总是20个字节的数据包。它是通过在数据上执行一个灵巧的位操作序列而创建的,这个序列在本质上可以百分之百地保证无论这些数据以何种方式发生变化,其指纹也都会跟着变化。(关于SHA的更多细节,可以查看一些参考资料,例如William Stallings所著的《Cryptography and Network Security: Principles and Practice》第7版[Prentice Hall, 2016]。)但是,序列化机制只使用了SHA码的前8个字节作为类的指纹。即便这样,当类的数据域或方法发生变化时,其指纹跟着变化的可能性还是非常大。
在读入一个对象时,会拿其指纹与它所属的类的当前指纹进行比对,如果它们不匹配,那么就说明这个类的定义在该对象被写出之后发生过变化,因此会产生一个异常。在实际情况下,类当然是会演化的,因此对于程序来说,读入较旧版本的对象可能是必需的。我们将在2.4.5节中讨论这个问题。
下面表示了类标识符是如何存储的:
- 72
- 2字节的类名长度
- 类名
- 8字节长的指纹
- 1字节长的标志
- 2字节长的数据域描述符的数量
- 数据域描述符
- 78(结束标记)
- 超类类型(如果没有就是70)
标志字节是由在java.io.ObjectStreamConstants中定义的3位掩码构成的:
我们会在本章稍后讨论Externalizable接口。可外部化的类提供了定制的接管其实例域输出的读写方法。我们要写出的这些类实现了Serializable接口,并且其标志值为02,而可序列化的java.util.Date类定义了它自己的readObject/writeObject方法,并且其标志值为03。
每个数据域描述符的格式如下:
- 1字节长的类型编码
- 2字节长的域名长度
- 域名
- 类名(如果域是对象)
其中类型编码是下列取值之一:
当类型编码为L时,域名后面紧跟域的类型。类名和域名字符串不是以字符串编码74开头的,但域类型是。域类型使用的是与域名稍有不同的编码机制,即本地方法使用的
格式。
例如,Employee类的薪水域被编码为:
下面是Employee类完整的类描述符:
这些描述符相当长,如果在文件中再次需要相同的类描述符,可以使用一种缩写版:
这个序列号将引用到前面已经描述过的类描述符,我们稍后将讨论编号模式。
对象将被存储为:
例如,下面展示的就是Employee对象如何存储:
正如你所看见的,数据文件包含了足够的信息来恢复这个Employee对象。
数组总是被存储成下面的格式:
在类描述符中的数组类名的格式与本地方法中使用的格式相同(它与在其他的类描述符中的类名稍微有些差异)。在这种格式中,类名以L开头,以分号结束。
例如,3个Employee对象构成的数组写出时就像下面一样:
注意,Employee对象数组的指纹与Employee类自身的指纹并不相同。
所有对象(包含数组和字符串)和所有的类描述符在存储到输出文件时都被赋予了一个序列号,这个数字以00 7E 00 00开头。
我们已经看到过,任何给定的类其完整的类描述符只保存一次,后续的描述符将引用它。例如,在前面的示例中,对Date类的重复引用就被编码为:
相同的机制还被用于对象。如果要写出一个对之前存储过的对象的引用,那么这个引用也会以完全相同的方式存储,即71后面跟随序列号,从上下文中可以很清楚地了解这个特殊的序列引用表示的是类描述符还是对象。
最后,空引用被存储为:
下面是前面小节中ObjectRefTest程序的带注释的输出。如果你喜欢,可以运行这个程序,然后查看其数据文件employee.dat的十六进制码,并将其与注释列表比较。在输出中接近结束部分的几行重要编码展示了对之前存储过的对象的引用。
当然,研究这些编码大概与阅读常用的电话号码簿一样枯燥。了解确切的文件格式确实不那么重要(除非你试图通过修改数据来达到不可告人的目的),但是对象流对其所包含的所有对象都有详细描述,并且这些充足的细节可以用来重构对象和对象数组,因此了解它还是大有益处的。
你应该记住:
- 对象流输出中包含所有对象的类型和数据域。
- 每个对象都被赋予一个序列号。
- 相同对象的重复出现将被存储为对这个对象的序列号的引用。