Java序列化【草案一】
(从09年回到重庆过后,就一直在工作,时间长了惰性就慢慢起来了,公司的项目从09年忙到了现在,一直没有时间来梳理自己的东西,CSDN的Blog似乎都荒废了,不知道现在还能否坚持把Blog完成,希望有一个新的开始吧!如果读者有问题还是可直接发邮件到silentbalanceyh@126.com,我也仅仅只是想把看的、写的、学的东西总结起来,让自己有个比较完整的学习记录。本文主要针对Java的序列化相关知识,先不涉及XML序列化和Json序列化的内容,这部分内容以后再议。着色的目的是强调重点和关键的概念以及防止读者通篇阅读的视觉疲劳,也是个人的写作风格,不习惯的读者请见谅!)
本章目录:
1.Java中的序列化
2.序列化原理和算法——基础数据
3.深入序列化规范
4.源码分析
----ObjectStreamField
----ObjectStreamClass
----ObjectOutputStream
----ObjectInputStream
5.序列化原理和算法——面向对象
1.Java中的序列化
Java是面向对象的计算机语言,序列化【Serialization】并不是Java语言独有的一种机制,它表示将一个对象的状态信息转换成为可存储或者可传输的数据格式的过程;在序列化过程中,对象的状态可以写入到临时或者永久的存储区,需要再次使用这个对象的时候,就用反序列化【Deserialize】的方式将该对象直接还原。在理解序列化之前首先需要理解的一个关键概念是:什么叫做对象的状态?
i.对象的状态
先看一段代码:
从上图可以知道,对机器本身而言,状态改变了5次,而代码里面的set*方法就是其状态修改的证据——但是并不是每一次状态修改都是人需要去关注的,从代码中的逻辑可以知道,其关注点有两处,这两处都有System.out.println语句,所以从人的角度来理解上边的代码其状态图如下:
对一个对象的状态描述可使用语句:”当对象的属性1为x,属性2为y,……的状态“。所以在理解对象状态的时候需要关注的是对象存在的一个瞬时性,在理解其状态基础之上再来理解序列化的概念就相对容易多了,上边的代码输出为:
ObjectStatus [name=LangYu, age=1]
ObjectStatus [name=LangYu, age=3]
1)关于初始状态
理解对象的初始状态对程序的Debug有很好的帮助,而理解其初始状态的基础是面向对象语言的语言特性,前面章节讲到过《Java的类和对象》,它讲解了Java对象中的成员属性的初始化规则。结合上边的图,思考这样的一个问题:为什么初始状态里的name的值为null,age的值为0呢?——其实它和Java语言中实例变量的初始化规则有关,这一点在这里就不详细说明了。还需要注意的一点是关于静态变量,Java中在谈到对象非类的时候,不涉及静态变量和定义的静态方法,静态域修饰的内容在Java里面隶属于类而不是对象,所以在对象状态里不包含静态成员。对象的初始化是开发员可自定义的,只有在未提供初始值的时候,JVM才会为对象的属性提供初始值,这里通过上边的name的定义来理解这一点:
区分语言中”存储“和”使用“也是理解序列化概念的一个基础知识点,简单说来:存储主要表现为维持对象状态,它具有的基本特性是持久性;而使用主要表现针对对象的瞬时状态的处理,该处理有可能改变对象的状态,有可能不改变对象的状态,它可以使用某一个瞬间对象的状态。
3)瞬时性 VS 持久性
区分瞬时性和持久性是区分序列化【Serialization】和持久化【Persistence】的基础,Java中的对象只具有瞬时性,不具有持久性。这样说读者可能难以理解,先考虑一个生活中常见的场景——照相。一般在照相的时候,照片上的内容只是截取的某一个时间点上的人或者事物的状态,并不是照相过后人和事物就保存在介质照片上了,人和事物这些对象并没有在存储介质中,而是随着时间轴的变化在继续运行;同样的道理,Java的对象本身如同人或者事物,而存储这个操作是触发了快门,存储介质上保存的仅仅是Java对象当时的状态,在存储代码执行完了过后,Java对象还是会在JVM中继续运行,只是它的状态被保存下来存储于某个介质。既然如此,Java对象的持久性怎么理解呢?回到照相这个隐喻:假设人和事物可以进入照片中的世界,那么面对同一张照片的时候,不论多少次人和事物恢复出来的状态都是一样的,而这个刚刚恢复过的状态因为照片这个介质就具有了持久性。上边这个Java例子中,只要数据库中的name和age不发生任何变更,则对象不论恢复多少次的初始化状态就始终是:
ObjectStatus [name=Lang, age=27]
也就是说:ObjectStatus在name的值为Lang,age的值为27的这种状态被永久保存下来了,存在哪儿?——存储介质中!这种情况下Java对象的特性就可以称为对象的持久性。
【*:有了上边的说明,请读者思考:Java对象和Java对象的状态有什么不同?】
ii.序列化
序列化【Serialization】从严格意义上讲是一种机制,而它所关注的点是“格式”,它的操作对象是将Java对象的某个状态【*:不是Java对象本身】转化为介质可接受的一种格式,这种格式方便传输、方便存储,而转化的这个过程称为序列化。——需要理解的是序列化过后的数据有可能被永久保存下来,也有可能使用过后就直接被回收。在Java语言中,对JVM而言这些序列化过后的数据就仅仅是数据了,数据中序列化过后的对象状态中对象的某个属性不能直接访问,若要访问则需要先执行反序列化操作,将这些特定格式的数据转化成为JVM可识别的Java对象。可以这样说:反序列化和序列化是一个互逆的过程,它表示把一种序列化过的特殊格式的数据转换成JVM可识别的Java对象的格式。在网络上进行数据通信的时候,无论是什么数据,都会被转换成为二进制序列【下文中都使用”二进制序列“作为术语,实际上就是二进制字节数据,也称为字节序列】的数据进行传输,一般情况下发送数据方都需要把这些数据转换成为字节序列,才能在网络上传输,所以原始的Java序列化和反序列化有一个比较广泛的定义:
随着网络的不断发达,有几种比较抽象的数据格式诞生,虽然这些格式的数据在传输底层依然使用的是二进制序列,但是从开发的角度上讲,Java对象序列化的目标格式出现了多元化,而本文未提及到的XML和JSON格式就是比较流行的两种【*:这两种格式较二进制序列格式的优势是让人易于阅读】,基于XML的数据格式的序列化称为XML序列化,基于JSON的数据格式的序列化称为JSON序列化。接下来先看看Java的内建序列化过程,请先看下边的代码:
对象实现该接口就可以在任何序列化场景使用该对象,一句简
单的implements Serializable语句就让Java对象具有了可序列
化的语义。使用比较复杂,JVM虚拟机不提供任何机制,需要让Java开发
人员自己去实现两个写入和读取方法的细节,readExternal方法
负责反序列化的实现细节,writeExternal方法负责序列化的实现
细节,这种方式对开发人员要求相对比较高。灵活程度不够灵活,开发人员无法控制序列化过程的细节操作。灵活,开发人员可自定义序列化过程的任何细节内容。性能对比占用空间比较大,有时候因为额外的开销使得序列化过程的速
度变得很慢,又由于开发人员无法控制序列化的细节,其性能
问题无法从直接编码的方式解决。具有性能的双面性,它的空间开销有可能很少,因为是由程序员
来定义需要存储什么以及不需要存储什么。若程序员在实现过程
处理得好的话,性能有可能会有所提升。
在选择使用这两个接口的时候需要根据应用程序的需求来决定,Serializable接口通常是最简单的方案,但是它有可能会导致不可接受的性能问题或空间问题;当这种情况发生的时候,使用Externalizable接口也许是一种不错的选择。看看下边的代码来理解Externalizable接口:
2对象方式out.writeObject(new Integer("3"));
73 71 00 7E 00 00 00 00 00 033对象方式out.writeObject(first);
71 00 7E 00 022引用方式out.writeObject(new Integer("4"));
73 71 00 7E 00 00 00 00 00 044对象方式out.writeObject(new Integer("5"));
73 71 00 7E 00 00 00 00 00 055对象方式out.writeObject(second);
73 71 00 7E 00 00 00 00 00 022对象方式out.writeObject(new Integer("6"));
73 71 00 7E 00 00 00 00 00 066对象方式out.writeObject(new Integer("7"));
73 71 00 7E 00 00 00 00 00 07
7对象方式out.writeObject(second);
71 00 7E 00 06
2引用方式out.writeObject(third);
……01true对象方式out.writeObject(new Integer("6"));
73 71 00 7E 00 00 00 00 00 066对象方式out.writeObject(second);
71 00 7E 00 062引用方式out.writeObject(first);
71 00 7E 00 022引用方式out.writeObject(third);
71 00 7E 00 0Atrue引用方式
先仔细看看上边的表格,通过分析来理解序列化中TC_REFERENCE的详细用法,把上边的表格总结下【为了把Java语言中的引用和序列化数据中的引用区分,下边总结部分”Java引用“表示Java语言中的引用,”引用“表示使用了TC_REFERENCE的序列化数据中的引用】:
区分Java语言的引用和TC_REFERENCE:从二进制序列可以看出,使用了标记【71】的序列就是在序列化中使用的引用TC_REFERENCE的部分,上述出现了12次;而Java语言的引用这里就不多说,上边有3个:first、second、third;从二进制序列可以看到,表示同一个Java引用的二进制序列应该是一模一样的,例如上述从第二次开始每次调用writeObject(first)的部分,其输出都为【71 00 7E 00 02】;但是TC_REFERENCE在使用的时候,其作用为:保证序列化后的数据格式中类描述信息【元数据部分】部分的唯一性,同类型的对象在序列化的时候,第一次序列化会生成类描述信息,之后都直接使用TC_REFERENCE操作;针对某一个类的对象,它在第一次序列化的时候会先输出类描述信息:【73 72】TC_OBJECT TC_CLASSDESC开始,【78 70】TC_ENDBLOCKDATA TC_NULL结束,随后跟上其对象中的属性值列表。Integer类中只有value属性,first这个Java引用对应的Integer对象其value属性值为2;Boolean类中也只有value属性,third这个Java引用对应的Boolean对象其value属性值为true,类似上使用了省略号被省略部分的二进制序列;关于Java对象的创建——Java在序列化的过程中,创建对象的顺序如下:1.若创建1个新的Java对象,输出【73】TC_OBJECT表示当前创建的是一个新的对象;若不创建新的Java对象,则输出【71】TC_REFERENCE标记;
2.判断当前环境中是否有创建对象的类描述信息【元数据部分】,如果没有类描述信息使用【72】TC_CLASSDESC标记,如果已经序列化过类描述信息则使用【71】TC_REFERENCE标记(已经输出过该标记不输出第二次);
3.类描述信息输出完成后(【78 70】结束),直接按照类中定义的属性顺序输出对象中属性的值列表;
4.如果是使用的【71】TC_REFERENCE标记,需要分为两种情况:创建Java新对象 or直接写入引用,其格式如下(接着【73 71】之后或者【71】之后):
——创建Java新对象:先输出【00 7E 00 00】baseWireHandle变量,每次创建一个新对象的时候都会输出该变量,随后跟上对象的属性值列表;
——直接写入对象的引用:输出【00 7E 00 XX】格式(至少可以支持创建65536个新引用),这种情况不需要追加对象的属性值列表;TC_REFERENCE的管理:【71】TC_REFERENCE标记之后,是一个整数Int类型的数据,它生成的基数是【00 7E 00 00】baseWireHandle常量,它的值表示了序列化中Java的新对象统计数据(其值的运算根据对象的hashCode方法运算而来)。基于这个规则看看表格中的数据:
【71 00 7E 00 02】(第三行)——它和第一行的Java对象引用相等,也就表示该引用引用的Java对象是第一行创建的,同理倒数第二行也是first引用生成的二进制序列,其生成的序列值一模一样【71 00 7E 00 02】;
【71 00 7E 00 06】(倒数第三行)——它和第六行的Java对象引用相等,也就表示该引用引用的Java对象是第六行创建的,与之对应的还有第九行的序列【71 00 7E 00 06】;
——最后需要注意一点的是:这个序列的起始值是2不是1,也就是说从【00 7E 00 02】开始,至于为什么希望在后边分析源码章节能够说明清楚,目前还不清楚详细的原因;
vi.基础类型做成员属性
上述标记中多次出现了类似71、72、78等各种具有语义的标记,在继续讲解之前先看看下边的内容。
TC_*标记表(位于接口java.io.ObjectStreamConstants,只列出了数据常量):
变量名称十六进制值十进制值含义baseWireHandle【00 7E 00 00】8257536
该值一般位于TC_REFERENCE之后,为计数器的基数,它一般表示第一个赋值的句柄;STREAM_MAGIC【AC ED】-21267Java序列化数据中输出到目标文件的“魔数”段STREAM_VERSION【00 05】5序列化协议中的版本信息,一般位于STREAM_MAGIC之后TC_ARRAY【75】117标记接下来序列化的内容是一个数组对象TC_BLOCKDATA【77】119标记接下来的一段数据是一个可选数据块的内容,跟随其后的int类型数字表示了之后的数据字节数TC_BLOCKDATALONG【7A】122同TC_BLOCKDATA,只是跟随其后的是一个long类型数字,它同样表示了数据字节数TC_CLASS【76】118该标记用于引用一个类,实际上此标记就是一个Class的引用标记TC_CLASSDESC【72】114该标记一般位于TC_OBJECT,用于描述当前序列化对象的类描述信息【元数据部分】TC_ENDBLOCKDATA【78】120该标记用于表示一个Java对象的描述结束,一般为对象描述终止TC_ENUM【7E】126该标记在JDK 1.5过后有效,表示接下来的数据是一个枚举常量值TC_EXCEPTION【7B】123该标记表示接下来的数据是一个异常对象,一般是一个Exception的对象TC_LONGSTRING【7C】124该标记表示接下来的数据是一个长字符串对象,一般是长度超过了某一个固定的值TC_MAX【7E】126该标记表示最后一个标记值TC_NULL【70】112此标记表示null,用于描述对象的空引用TC_OBJECT【73】115该标记是一个新对象的声明,表示接下来的数据是新创建的一个对象TC_PROXYCLASSDESC【7D】125该标记一般位于TC_OBJECT之后,表示当前Java对象是一个代理类对象TC_REFERENCE【71】113该标记表示引用,其表示接下来的数据类型是Java引用类型TC_RESET【79】121重置标记,意味着对象流中的数据会被重置TC_STRING【74】116该标记表示当前序列化对象是一个new String的字符串对象 对于对象中的属性,它的类型标记和上述表格中的标记不一致,上边提到过:【4C】是字符'L',它表示当前属性是一个String,接下来看看类型的编码对应表【只针对对象中的成员属性的类型】:十六进制值对应的字符字段的类型42Bbyte43Cchar44Ddouble46Ffloat49Iint4AJlong4CL类或者接口类型53Sshort5AZboolean5B[数组类型,array 接下来的代码演示的是基础类型作为对象中的成员属性的情况:
3)读取对象流
Java语言中从流数据中读取Java对象的过程如下:
4)对象流容器
Java中的对象序列化机制生产和消费的都是字节流数据【上边示例中的二进制序列】,这些字节流里面可能包含一个或多个Java基础类型数据以及Java对象数据——如果Java对象写入到流数据中引用了其他Java对象,这个字节流中同样也会描述这种关系。实际上Java对象充当了一个流数据容器,它提供了读取和写入字节流数据的接口,这两个接口就是ObjectOutput和ObjectInput:
如果一个Java对象要充当序列化中的流容器,它必须显示声明自己符合了JVM的序列化协议【通过实现java.io.Serializable接口】,这样的Java对象才能将自己的状态写入字节流【序列化】以及从字节流中读取Java对象状态重建该Java对象【反序列化】。JVM中定义了两套协议用于这种操作:
- 实现Serializable接口实现Externalizable接口
5)类中定义”可序列化“字段
在一个类中定义”可序列化“的字段有两种不同的办法;默认情况下——一个类里面只要字段的定义是非transient或者非静态的定义【不使用transient和static关键字】,那么这种字段就是可序列化的,使用Java的内建序列化进行处理。另外一种情况——定义可序列化的字段是在一个实现了Serializable接口的类中重写成员属性serialPersistentFields,这个属性的类型必须是一个ObjectStreamField的数组【ObjectStreamField[]】,这个数组枚举了所有需要序列化的字段名称和值的集合,而且这个属性的修饰符必须是固定的,其格式如下:
和上边讨论的目的一样,每一个类需要实现和继承于某个接口或者和父类之间定义的合约。新版的类,例如图中的foo',必须满足foo拥有的合约而且它有可能实现某个接口而修改其实现。通过序列化在对象之间进行通信并不属于这些接口定义的合约,序列化是各种实现之间的一个私有协议,它的责任是使得所有的实现能够有效地通信,同时允许每一个实现继续满足其客户端期望的合约【这里把和某个对象通信的另一端称为其客户端】。
4)兼容的Java类型演化
Java的语言规范讨论了Java中的类在进化过程其二进制码的兼容性,大部分二进制码的兼容性来自于Java中类、接口、字段、方法等符号引用的延迟绑定。
下边是设计Java中可序列化的对象流的原则:
-- 一个对象中字段的必须数据部分的组成顺序是由类描述符定义的;
-- 可选数据部分在写入字节流的时候和类中的成员属性并不直接对应,类定义负责描述这些数据的长度、类型、以及可选信息的版本号;如果一个类中定义了writeObject/readObject方法,则这个方法将会取代默认序列化机制中的方法写入/读取对象的状态,可选的信息可依靠这些方法写入或者读取,而必须数据部分可依赖defaultWriteObject方法和defaultReadObject方法;字节流格式用来标识每一个类的方式是使用SUID(Stream Unique Identifier),默认情况下该值是类的哈希值。所有最新版本的类都必须定义这些类能兼容的SUID,这样可以防止同名类出现,如同单个类的版本一样随意标识也不会混淆;ObjectOutputStream和ObjectInputStream的子类可以包含它们拥有的信息使用annotateClass来标识类,就像URL中使用的MarshalOutputStream类一样;
5)类型变化影响序列化
基于上边的概念,我们现在可以描述如何设计针对类进化的不同情况,类的某些版本从字节流写入的角度描述了这些情况,当这些字节流被同样版本的类读回时,不会出现功能和信息的丢失,对原始的类而言只有字节流是信息源。它的类描述——原始类的类描述集,足以与重组类的版本字节流中的数据匹配。
不兼容的改变【无法维护互操作性的变更】:
删除字段如果一个类中删除了一个字段,则字节流的写入将不包含该字段的值。当早期版本读取该字节流的时候,因为字节流中没有该字段可用的数据,这个字段的值会被设置成默认值。尽管如此这个默认值有可能损害早期版本的能力来履行它的代码合约;在继承树中上下移动该类
因为这样会导致字节流出现错误的顺序,所以不允许;将一个非静态字段或者非transient字段修改成静态【static】或transient
若依赖于Java默认的序列化,这种改变等价于从类中删除了一个字段。这个字段中的数据不会写入字节流,早期版本的类无法读取该字段的数据;和删除字段一样,这个字段会使用默认值进行初始化,这样有可能导致类相关操作以意外的方式失败;改变基础类型字段的定义类型
类的每一个版本都会基于字段类型定义写入字段数据,早期类的版本在读取字段数据的时候会因为和字节流中字段类型不匹配而失败;改变writeObject和readObject方法
这样它不再写入或读取默认字段数据,改变了这些方法过后新版本会尝试读取和写入数据但之前的版本不会这样做。默认的字段数据必须始终如一地在字节流中出现或者不出现;将类的序列化和外部化交换【Serializable和Externalizable交换】
因为合法类的实现包含了不兼容的数据,所以这种改变是不兼容的;枚举类Enum和非枚举类型相互交换
同上,因为数据是不兼容的,所以这种改变不兼容;移除类的序列化和外部化
写入的数据无法再满足老版本的类的需要,所以是不兼容的改变;为类添加了writeReplace或者readResolve方法
这些行为将直接生产和旧版本的类不兼容的对象,所以是不兼容的改变;
兼容的改变:
添加字段当一个类重组过后,若它添加了一个字节流中没有出现的字段,该类实例化的字段将使用它默认的类型和值进行初始化。如果需要该类特殊的初始化,这个类必须提供一个readObject方法的重写,这样就可以使用非默认值对该字段执行初始化;添加类
字节流将包含该流中每一个对象的类型继承树,并且它会比较该继承树结构与当前类字节流中其他可检测到的类型有何不同。如果字节流中没有信息能执行对象的初始化,则对应类的字段将使用默认值初始化对象;移除类
对比字节流中的继承树结构和当前类能检测被删除掉的类,这种情况下,该类中的对象和字段会直接从字节流中读取。基础类型数据会被放弃,但是被删除的类对应的对象引用会被创建,因为这些引用会在后边的字节流中使用。如果字节流中出现了垃圾回收【Garbage-Collected】或者重置【Reset】标记,这些类会被垃圾回收机制处理;添加writeObject/readObject方法
如果字节流的版本读取拥有所期望的readObject方法,和平常一样将使用默认的序列化机制读取写入到字节流的必须数据。在读取任意可选的数据之前,优先调用defaultReadObject方法,而writeObject方法将会调用defaultReadObject方法去写入必须数据,接着有可能会写入可选的数据;移除writeObject/readObject方法
如果读取字节流的类没有这些方法,这些必须数据将会被默认的序列化机制读取,而这种情况下可选数据将会被放弃;添加接口java.io.Serializable实现
这种方式和添加类型是等价的,字节流中没有任何值提供给Java类所以这个类的字段会使用默认值执行初始化。非序列化子类的支持要求其父类必须存在无参构造函数而且这些类本身需要使用默认值初始化,如果无参构造函数是无效的,则会抛出InvalidClassException异常;改变字段的访问控制修饰符
访问控制修饰符如public、private、默认域、protected对序列化没有任何影响,所以它们不影响字段的赋值;将字段从静态【static】和transient改成非静态或非transient
当系统依赖默认序列化机制计算并且序列化字段时,这种改变等价于在类中添加新字段。这些新的字段将会写入到字节流,但是早期版本的类会忽略这些值因为序列化将不会为静态【static】或transient字段赋值;
vi.对象序列化流协议
对象字节流的格式将满足下边的设计目标:
针对高效读取必须简单并且是结构化的;允许仅仅使用结构化数据的知识和字节流格式跳过字节流读取,这种情况不需要调用任何类的代码;仅仅允许字节流本身操作该数据;1)字节流元素【或者称为项】
基本的结构需要描述字节流中的对象,对象的每一种属性都应该在字节流中体现:对象所属类、对象的成员属性【字段】,这些数据会被写入而且之后会被类中特定的方法读取,字节流中对象的描述会使用固定的语法。null对象、新对象【new objects】、类【classes】、数组【arrays】、字符串【strings】、任何在流中存在的对象的反向引用【back references】都会有特殊的描述信息,每一个写入字节流的对象都会被赋予引用Handle,这个引用Handle可以反向引用该对象。该引用Handle会从0x7E0000开始进行顺序赋值并且自动自增,一旦字节流发生了重置则该引用Handle会重新从0x7E0000开始。
一个类的实例化对象使用下边结构描述:
一个类的ObjectStreamClass对象描述非动态代理类按照下边规则:
可兼容类的SUID【Stream Unique Identifier】标记集合用来指示一个类的各种属性,包括该类是否定义了writeObject方法、是否可序列化、是否是可外部化的、是否是枚举Enum类型;可序列化的字段的数量;一个类的字段数组会被默认的序列化机制序列化,对于数组和对象的字段而言,字段的类型会包含在一个描述字符串中,它必须是“字段描述符【field descriptor】”格式(类似"Ljava/lang/Object;"),这个格式在虚拟机规范中有说明;可选的数据块记录【Data-Block】和使用annotateClass方法写入字节流的对象;当前对象的超类的ObjectStreamClass(如果超类不可序列化则返回null);一个类的ObjectStreamClass对象描述动态代理类时按照下边规则:
该动态代理类实现的接口的数量;所有动态代理类实现的接口的名称列表,这些接口通过调用Class的getInterfaces方法的返回结果进行排序;可选的数据块记录【Data-Block】和使用annotateProxyClass方法写入字节流的对象;当前对象的超类对应的ObjectStreamClass,java.lang.reflect.Proxy; 字符串对象的描述信息会在字段描述符【field descriptor】之后紧跟着字符串的长度,之后再跟着通过UTF-8编码过的字符串内容部分。修改过的UTF-8编码和Java虚拟机中的,java.io.DataInput和java.io.DataOutput接口中的一样;它和标准UTF-8格式中描述补充字符和null字符的表现形式有所不同,长度信息在字节流中的表现形式取决于修改过的UTF-8编码过的字符串的长度。如果UTF-8编码过的字符串长度小于65536字节的长度,则写入2个字节的16-bit无符号整数;从JDK 1.3开始,UTF-8编码过的字符串长度如果大于等于65536个字节,则写入8个字节的64-bit无符号整数,在序列化字节流中,字符串之前的类型编码【Type Code】表示写入的String字符串使用的哪种格式。
数组类型的数据描述信息包含下边内容:
Enum枚举常量的描述信息包含下边内容:
基于Enum类型的枚举常量的ObjectStreamClass对象;枚举常量的名称,调用name()方法的返回结果;新对象【New objects】的描述信息包含下边内容:
所有对象类的派生类信息;对象的每一个可序列化类的数据,从它的顶级父类开始写入。针对字节流中的每一个类的信息包含下边内容:-- 一个类中的可序列化字段信息;
-- 如果这个类包含writeObject/readObject方法,有可能出现通过writeObject方法写入的可选对象或者基础类型的数据块【Data-Block】记录,跟着使用endDataBlock的代码;
所有被类写入流的基础类型的数据都会被缓冲以及包装在【Data-Block】数据块记录中,无论如何这些数据都会在writeObject方法内写入流或者直接在writeObject方法之外直接写入字节流,这些数据只能被对应的readObject方法读取或则直接从字节流中读取。writeObject方法写入对象数据的时候会直接结束掉之前的数据块【Data-Block】记录,然后按照系统期望写入标准Java对象【Regular Object】、空对象【null】或者反向引用【Back Reference】。数据块【Data-Block】记录允许放弃任何可选数据进行错误修复。若从一个类中调用时,字节流可以放弃任何数据或者对象直到endDataBlock标记出现。
2)字节流协议版本
在JDK 1.2中,有必要修改和JDK 1.1不兼容的字节流格式;为了处理这种情况,向前兼容性是必须的,一个兼容标记将会写入到字节流中,这个兼容标记是类似PROTOCOL_VERSION的格式,ObjectOutputStream中的useProtocolVersion方法会接收一个参数以表示写入的可序列化字节流的协议版本。
使用的字节流协议版本如下:
数据块的边界是标准化的,使用数据块模式写入字节流的基础类型的数据通常不能超过1024字节长度,这种变化的好处是固定以及规范化序列化数据格式,有利于其向前和向后的兼容性。
JDK 1.2默认使用PROTOCOL_VERSION_2
JDK 1.1默认使用PROTOCOL_VERSION_1
JDK 1.1.7版本以及以上的版本可读取以上的两种版本,而JDK 1.1.7之前的版本只能读取PROTOCOL_VERSION_1版本;
3)字节流格式的语法
下边的表包含了字节流格式的语法,非终结符号以斜体显示,终结符号拥有固定的宽度;非终结符号的定义之后带了一个“:”,这个定义之后每一行会有一个或者多个替代符号。看看下表的基本语义:
标记格式含义(datatype)这个标记表示数据类型,例如byte。token[n]预定义标记的数量,也是一个数组匹配项的数目。x000116进制数据格式的字面量,16进制位的数量反应了值的大小。<xxx>从数据流中读取用来表示数组长度的一个值。语法规则【根标记为蓝色;还需要继续解析的标记为红色;已经可以使用的最小单位为黄色,表示终止符。】:
stream:
magic version contents
整个数据流的格式,直接分成三部分,magic表示魔数STREAM_MAGIC标记,version表示序列化的版本STREAM_VERSION,contents表示最终生成的序列的内容;
contents:
content
contents content
这一部分表示生成的二进制序列的内容部分,这些内容有可能是独立的内容【content】,也可能是多个内容的一个集合【contents】;
content:
object
blockdata
二进制序列独立的内容【content】有可能包含对象定义的数据【object】,也有可能包含数据块格式的数据【blockdata】,上边格式也有能blockdata在前,object在后;
object:
newObject
newClass
newArray
newString
newEnum
newClassDesc
prevObject
nullReference
exception
TC_RESET
该部分内容表示对象中包含的字节流数据,这部分数据中的元素相互间没有顺序,仅仅表示该对象中可能存在标记表示的数据;newObject表示新对象类型, newClass表示Class类型的对象,newArray表示数组对象,newString表示字符串对象,newEnum表示枚举常量,newClassDesc表示对象的类描述信息,preObject表示前边出现过的对象,nullReference表示空引用,exception表示异常对象,TC_RESET表示重置标记【固定值】。
newClass:
TC_CLASS classDesc newHandle
该部分内容表示一个新的Class类型的对象,TC_CLASS表示类型标记,classDesc表示类描述信息,newHandle表示新的引用;
classDesc:
newClassDesc
nullReference
(ClassDesc)prevObject
该部分表示一个对象的类描述符,newClassDesc表示新出现一个类描述符,nullReference表示空引用,prevObject表示前边出现过的对象;
superClassDesc:
classDesc
这部分表示父类的描述符信息,它的内容是一个classDesc,也就是上边类描述信息;
newClassDesc:
TC_CLASSDESC className serialVersionUID newHandle classDescInfo
TC_PROXYCLASSDESC newHandle proxyClassDescInfo
这个部分演示了类描述符中描述的两种类描述符信息:一般类描述信息,动态代理类描述信息,clsssName表示类名,serialVersionUID表示该类中定义的serialVersionUID对应的值,newHandle表示一个新的引用,classDescInfo表示类描述符本身的相关信息,proxyClassDescInfo表示动态代理类描述符本身相关的信息;
classDescInfo:
classDescFlags fields classAnnotation superClassDesc
这一部分内容是详细的类描述信息,classDescFlags为类描述信息标记,fields表示类中所有字段的描述信息,classAnnotation表示和类相关的Annotation的描述信息,superClassDesc表示该类的父类的描述信息。
className:
(utf)
类全名,以UTF-8的格式保存的字符串对应的二进制序列,描述了当前对象的类全名;
serialVersionUID:
(long)
对应类定义中的字段serialVersionUID的信息;
classDescFlags:
(byte)
类描述符标记,一个字节的数据,用于定义终止符和常量;
proxyClassDescInfo:
(int)<count> proxyInterfaceName[count] classAnnotation superClassDesc
动态代理类的相关描述信息,<count>表示该动态代理类实现的接口总数,类型为int类型。proxyInterfaceName[count]表示所有当前动态代理类实现的接口信息,classAnnotation表示该动态代理类对应的Annotation的描述信息,superClassDesc表示当前动态代理类的父类的类描述信息;
proxyInterfaceName:
(utf)
动态代理类的代理接口的名称,一个UTF-8格式的字符串对应的二进制序列;
fields:
(short)<count> fieldDesc[count]
<count>该类中的字段【成员属性】的总数,数据类型为short类型。fieldDesc[count]表示一个类中所有字段的详细描述信息,字段的数量和前边的count是一致的;
fieldDesc:
primitiveDesc
objectDesc
这个标记表示字段的描述信息,字段描述信息包括两部分信息内容,primitiveDesc表示基础类型数据的描述信息,objectDesc表示对象类型数据的描述信息;
primitiveDesc:
prim_typecode fieldName
基础类型的字段的相关描述信息,prim_typecode表示字段的类型标识,字段类型标识表示当前字段的类型,fieldName表示字段的名称,为一个字段名称组成的字符串的二进制序列;
objectDesc:
obj_typecode fieldName className1
对象类型的字段的描述信息,obj_typecode表示字段的类型标识,该标识描述了对象字段对应的类信息,fieldName表示字段的名称,为一个字段名称组成的字符串的二进制序列,className1表示该成员属性的类型签名;
fieldName:
(utf)
字段名称字符串组成的二进制序列,其字符串为经过UTF-8编码的内容;
className1:
(String)object
该对象对应的类的类全名,为一个String类型的对象描述信息;
classAnnotation:
endBlockData
contents endBlockData
该对象所属类中的Annotation的描述信息,endBlockData为存储对象的数据块【Data-Block】的结束标记,为终止符,contents表示该类中多个内容的一个集合【contents】;
prim_typecode:
'B'// byte
'C'// char
'D'// double
'F'// float
'I'// integer
'J'// long
'S'// short
'Z'// boolean
基础类型的字段的类型标识,标识了字段所属的基础数据类型,其代码表示的类型含义如定义中的注释部分的内容;
obj_typecode:
'['// array
'L'// object
对象类型的字段的类型标识,标识了字段所属的对象类型,其代码表示类型含义如注释部分的内容;
newArray:
TC_ARRAY classDesc newHandle (int)<size> values[size]
创建一个新的数组的描述符,TC_ARRAY表示接下来的序列是一个数组,它是数组序列的开始标记,classDesc是当前这个数组的类描述符,newHandle表示针对当前数组对象的引用,<size>表示该数组的长度,长度数字为int类型,values[size]表示当前数组每一个元素的值部分的内容;
newObject:
TC_OBJECT classDesc newHandle classdata[]
创建一个新的对象的描述符信息,TC_OBJECT表示接下来的序列是一个新对象,它是对象的开始标记,classDesc是当前这个对象的类描述符,newHandle表示针对当前对象的引用,classdata[]这个对象对应的每一个Class的相关数据信息;
classdata:
nowrclass// SC_SERIALIZABLE & classDescFlag && !(SC_WRITE_METHOD & classDescFlags)
wrclass objectAnnotation// SC_SERIALIZABLE & classDescFlag && SC_WRITE_METHOD & classDescFlags
externalContents// SC_EXTERNALIZABLE & classDescFlag && !(SC_BLOCKDATA & classDescFlags)
objectAnnotation// SC_EXTERNALIZABLE & classDescFlag && SC_BLOCKDATA & classDescFlags
这一部分数据描述的是类数据中所有内容,下边有针对各种不同的类数据相关说明;
nowrclass:
values
一个类中可序列化的字段的数据值,这些数据值的顺序遵循类描述符中定义的顺序;
wrclass:
nowrclass
这部分数据的内容和上述的nowrclass部分的内容是一样的;
objectAnnotation:
endBlockData
contents endBlockData
这部分数据的内容和classAnnotation的数据结构是一致的;
blockdata:
blockdatashort
blockdatalong
在Java序列化中,数据块存储分为两种:一种是长度为short的默认数据块方式,另外一种是长度为int的数据块方式,这种方式可存储容量大的数据;
blockdatashort:
TC_BLOCKDATA (unsigned byte)<size> (byte)[size]
描述了长度为short的默认数据块的结构;
blockdatalong:
TC_BLOCKDATALONG (int)<size> (byte)[size]
描述了长度为int类型的数据块的结构;
endBlockData:
TC_ENDBLOCKDATA
表示数据块的结束标记,一般用于描述当前的数据块结束了或者这个对象类型的描述符已经结束了;
externalContent:
(bytes)
object
这个部分描述的是外部化的相关内容,(bytes)部分的数据只能被readExternal方法读取,而且里面一般包含的数据类型是基础类型数据,object表示对象数据类型;
externalContents:
externalContent
externalContents externalContent
这部分内容是上述的外部化内容的一个集合,一般这一部分只包含了使用writeExternal方法以PROTOCOL_VERSION_1的版本写入字节流的数据;
newString:
TC_STRING newHandle (utf)
TC_LONGSTRING newHandle (long-utf)
表示一个字符串类型的数据,而字符串数据同样有两种类型:STRING和LONGSTRING;
newEnum:
TC_ENUM classDesc newHandle enumConstantName
表示一个Enum类型的数据,TC_ENUM为枚举类型的标识,表示接下来的序列类型是枚举类型,classDesc为一个枚举类型的类描述符,newHandle为该枚举对象的引用,enumConstantName的值为调用枚举类型中的name()方法返回的枚举类型的值对应的字符串字面量;
enumConstantName:
(String)object
枚举常量的字符串名称字面量,本身为一个字符串;
prevObject:
TC_REFERENCE (int)handle
表示已经写入到字节流中的对象的一个对象的引用,TC_REFERENCE上边已经说明过,这个标记是使用引用的标记;
nullReference:
TC_NULL
就一个字节长度的数据,就表示null值,一般这个值表示的是对象的空引用;
exception:
TC_EXCEPTION reset (Throwable)object reset
针对异常信息的描述,TC_EXCEPTION为异常信息的标记,标识接下来的序列是一个异常对象;
magic:
STREAM_MAGIC
魔数;
version:
STREAM_VERSION
序列化的版本信息,本文中使用的默认值是05;
values:
针对当前对象的classDesc对应的类描述信息提供描述类型的大小;
newHandle:
序列中的下一个数值将赋值给一个可序列化或者可执行反序列化的对象引用;
reset:
一个已知对象的集合将会被放弃,重置该字节流;
4)终止符
前一个章节已经介绍过TC*标记,这里再复习下,这些终止符标记在java.io.ObjectStreamConstants中定义:
上图中绿色部分是子节点,有些地方没有子节点是因为图中针对语法树已经有说明了,所以请读者阅读上图的时候细心!上边的图的清晰度不是特别高,有兴趣的读者可以自己去分析上边的语法自己绘制一颗语法树来体会Java序列化的目标数据的二进制格式,再结合前边章节提供的示例彻底理解Java中的内建序列化。