(从09年回到重庆过后,就一直在工作,时间长了惰性就慢慢起来了,公司的项目从09年忙到了现在,一直没有时间来梳理自己的东西,CSDN的Blog似乎都荒废了,不知道现在还能否坚持把Blog完成,希望有一个新的开始吧!如果读者有问题还是可直接发邮件到[email protected],我也仅仅只是想把看的、写的、学的东西总结起来,让自己有个比较完整的学习记录。本文主要针对Java的序列化相关知识,先不涉及XML序列化和Json序列化的内容,这部分内容以后再议。着色的目的是强调重点和关键的概念以及防止读者通篇阅读的视觉疲劳,也是个人的写作风格,不习惯的读者请见谅!)
本章目录:
1.Java中的序列化
2.序列化原理和算法——基础数据
3.深入序列化规范
4.源码分析
----ObjectStreamField
----ObjectStreamClass
----ObjectOutputStream
----ObjectInputStream
5.序列化原理和算法——面向对象
1.Java中的序列化
Java是面向对象的计算机语言,序列化【Serialization】并不是Java语言独有的一种机制,它表示将一个对象的状态信息转换成为可存储或者可传输的数据格式的过程;在序列化过程中,对象的状态可以写入到临时或者永久的存储区,需要再次使用这个对象的时候,就用反序列化【Deserialize】的方式将该对象直接还原。在理解序列化之前首先需要理解的一个关键概念是:什么叫做对象的状态?
i.对象的状态
先看一段代码:
package org.susan.java.serial; public class ObjectStatus { public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } private String name; private int age; @Override public String toString() { return "ObjectStatus [name=" + name + ", age=" + age + "]"; } public static void main(String args[]){ // 构造对象,该对象为一个初始状态 ObjectStatus obj = new ObjectStatus(); // obj的状态改变 obj.setName("LangYu"); obj.setAge(1); System.out.println(obj); // obj的状态改变 obj.setAge(3); System.out.println(obj); // obj的状态改变 obj.setAge(10); obj.setName("Lang Yu"); } }上边部分的代码很简单,先看看上边代码需要说明的内容。obj引用指向的对象是一个ObjectStatus类型的对象,该对象有两个基本属性:name和age。那么什么叫做对象的状态呢?——状态这个概念在网上一直有争议,这里归纳一下个人观点:理解对象的状态要从两个不同的角度进行,机器的角度和人的角度;从机器的角度来讲,对象的状态表示存储空间中对象的各个属性的瞬时值的一个集合,每修改一次对象的属性,对计算机本身而言其状态已经发生了改变;从人的角度来讲,对象的状态表示人在关注对象的时候对象在关注点的各个属性的瞬时值的一个集合,而我们在编程过程理解的对象状态一般只考虑关注点的对象。看看上边代码里面的对象的状态变化【初始化状态为对象状态变化的起点】:
从上图可以知道,对机器本身而言,状态改变了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的定义来理解这一点:
private String name;
在name属性的定义中并没有为name设置一个初始值,所以会使用JVM提供的系统初始值null,若使用下边的定义:
private String name = "Lang Yu";
这种情况下,name就不使用初始值null,而是使用定义的时候开发员提供的初始值Lang Yu,这一点必须要注意——简单讲:Java中对象的属性只有在开发员未提供初始值的情况下才使用JVM提供的系统默认值。理解了对象中每个成员属性的初始值,才能更加深入去理解对象的初始状态的所有细节,这也是理解对象状态的一个必要的知识点。
2)关于”存储“
关注点主要集中在”对象的使用“,而这种使用不包括”存储“,那么”存储“的意义在于什么呢?——存储从逻辑上讲,它重新定义了对象的”初始状态“。假设上边的代码中obj引用指向的对象创建的方式不是使用new关键字,而是从数据库读取的或者反序列化得到的,那么变更过的状态图如下【假设数据库中的内容为:name=Lang,age=27】:
区分语言中”存储“和”使用“也是理解序列化概念的一个基础知识点,简单说来:存储主要表现为维持对象状态,它具有的基本特性是持久性;而使用主要表现针对对象的瞬时状态的处理,该处理有可能改变对象的状态,有可能不改变对象的状态,它可以使用某一个瞬间对象的状态。
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的内建序列化过程,请先看下边的代码:
package org.susan.java.serial; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class SerialPerson implements Serializable{ /** * */ private static final long serialVersionUID = 1041712221752728541L; @Override public String toString() { return "SerialPerson [name=" + name + ", age=" + age + "]"; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } private String name; private int age; // 运行函数 public static void main(String args[]) throws Exception{ ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serial.obj")); // 序列化对象 SerialPerson person = new SerialPerson(); person.setAge(27); person.setName("Lang Yu"); // 数据的序列化写入 out.writeBoolean(true); out.writeObject(person); out.writeInt(120); out.writeObject("[email protected]"); out.close(); // 反序列化对象 ObjectInputStream in = new ObjectInputStream(new FileInputStream("serial.obj")); // 数据的反序列化读取 System.out.println(in.readBoolean()); System.out.println(in.readObject()); System.out.println(in.readInt()); System.out.println(in.readObject()); in.close(); } }
上边的代码使用Java序列化一个对象以及基础数据到一个serial.obj文件,然后从这个文件中反序列化这些数据并且还原,先给出这段代码的输出信息,然后仔细来看看代码中涉及的概念:
true
SerialPerson [name=Lang Yu, age=27]
120
[email protected]
1)Java内建序列化
Java序列化的实现需要依赖几个主要的接口和类:Serializable、Externalizable、ObjectOutputStream、ObjectInputStream,这几个主要的类的用法如下:
java.io.Serializable——一个Java的类若实现了Serializable接口,则该类的实例就启用了序列化功能,若该类未实现此接口则在Java内建【原生】序列化和反序列化的过程就会报错。此接口有一点需要注意:这个接口内未定义任何方法和字段,仅仅用来表示可序列化的语义。若对象没有实现该接口在执行内建序列化的时候会报如下错误:
Exception in thread "main" java.io.NotSerializableException: org.susan.java.serial.SerialPerson
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1180)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:346)
at org.susan.java.serial.SerialPerson.main(SerialPerson.java:41)
Java中java.io.NotSerializableException异常主要用于运行时检查这种情况,也就是说Java语言严格定义需要进行序列化操作的对象必须带有“可序列化的语义”。
java.io.Externalizable——当一个Java对象需要带有序列化的语义的时候,常用的方式是实现Serializable接口,该接口为Java对象的序列化提供了内建支持,会使用默认方式序列化Java对象。而Java里面提供了一种自定义序列化过程的方法,就是让对象实现Externalizable接口,此接口和Serializable接口不同的地方在于:此接口必须让实现者自己提供下边两个读取和写入的实现:
void readExternal(ObjectInput in)
void writeExternal(ObjectOutput out)
这两种方式都让Java对象具有了“可序列化的语义”,只是它们是实现同一目标的两种不同的方法,Serializable接口不需要实现者提供任何读取写入的实现,而Externalizable必须让实现者自己提供读取和写入的详细实现,实际上Externalizable接口是Serializable接口的子接口。对比一下这两个接口:
接口名称 | Serializable | Externalizable |
实现方法 | 内建支持,使用Java自身拥有的API | 需要实现者提供读取和写入方法的实现细节 |
使用难易 | 使用简单,开发人员在使用的时候只需要直接让需要序列化的 对象实现该接口就可以在任何序列化场景使用该对象,一句简 单的implements Serializable语句就让Java对象具有了可序列 化的语义。 |
使用比较复杂,JVM虚拟机不提供任何机制,需要让Java开发 人员自己去实现两个写入和读取方法的细节,readExternal方法 负责反序列化的实现细节,writeExternal方法负责序列化的实现 细节,这种方式对开发人员要求相对比较高。 |
灵活程度 | 不够灵活,开发人员无法控制序列化过程的细节操作。 | 灵活,开发人员可自定义序列化过程的任何细节内容。 |
性能对比 | 占用空间比较大,有时候因为额外的开销使得序列化过程的速 度变得很慢,又由于开发人员无法控制序列化的细节,其性能 问题无法从直接编码的方式解决。 |
具有性能的双面性,它的空间开销有可能很少,因为是由程序员 来定义需要存储什么以及不需要存储什么。若程序员在实现过程 处理得好的话,性能有可能会有所提升。 |
在选择使用这两个接口的时候需要根据应用程序的需求来决定,Serializable接口通常是最简单的方案,但是它有可能会导致不可接受的性能问题或空间问题;当这种情况发生的时候,使用Externalizable接口也许是一种不错的选择。看看下边的代码来理解Externalizable接口:
package org.susan.java.serial; import java.io.Externalizable; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectInputStream; import java.io.ObjectOutput; import java.io.ObjectOutputStream; public class ExternalObject implements Externalizable{ @Override public String toString() { return "ExternalObject [name=" + name + ", age=" + age + "]"; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } private String name; private int age; public void writeExternal(ObjectOutput out) throws IOException { out.writeObject("[name=" + getName() + "]"); out.writeInt(getAge()); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { this.name = (String)in.readObject(); this.age = in.readInt(); } // 运行函数 public static void main(String args[]) throws Exception{ ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("exserial.obj")); // 序列化对象 ExternalObject person = new ExternalObject(); person.setAge(27); person.setName("Lang Yu"); // 数据的序列化写入 out.writeObject(person); // writeExternal 自动执行 out.close(); // 反序列化对象 ObjectInputStream in = new ObjectInputStream(new FileInputStream("exserial.obj")); // 数据的反序列化读取 ExternalObject target = (ExternalObject)in.readObject(); System.out.println(target); // readExternal 自动执行 in.close(); } }上边的代码演示了Externalizable接口的基本用法,后边的章节会详细解析serial.obj文件和这里生成的extserial.obj文件来分析内建序列化和自定义序列化【外部化】的差异,上述代码的输出为:
java.io.ObjectInputStream & java.io.ObjectOutputStream——在Java语言里,这两个类又称为对象流输入输出类,ObjectOutputStream负责将Java对象的基本数据、对象数据以及图形数据等写入OutputStream,并且可使用ObjectInputStream进行读取,恢复【重构】该对象——实际上是读取该对象序列化时的状态,然后根据它的状态重新创建一个属性数据和原始对象一样的新对象,可以看做原始对象的副本,从数据使用上讲其用法和原始对象的使用是一致的,所以又可以称为“恢复&重构原始对象”;通过在流中使用目标介质文件【类似前边的serial.obj和extserial.obj】可以实现对象的持久存储,若是网络套接字,则可实现在另一台主机以及另一个进程中恢复【重构】该对象。
这两个类对其操作目标对象有几个注意点需要说明【仅针对默认的内建API实现】:
2)关键字transient
transient关键字在Java里是一个针对序列化的特殊关键字,被它修饰过的域具有“不会序列化”的语义,这个关键字只能用来修饰成员属性,而不可以修饰成员方法以及类。先看一段代码来理解这个特殊的关键字:
package org.susan.java.serial; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class TransientKeyword implements Serializable{ /** * */ private static final long serialVersionUID = -8670388050122803308L; @Override public String toString() { return "TransientKeyword [name=" + name + ", age=" + age + "]"; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } private transient String name; private int age; // 运行函数 public static void main(String args[]) throws Exception{ ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("transient.obj")); // 序列化对象 TransientKeyword person = new TransientKeyword(); person.setAge(27); person.setName("Lang Yu"); // 数据的序列化写入 out.writeBoolean(true); out.writeObject(person); out.writeInt(120); out.writeObject("[email protected]"); out.close(); // 反序列化对象 ObjectInputStream in = new ObjectInputStream(new FileInputStream("transient.obj")); // 数据的反序列化读取 System.out.println(in.readBoolean()); System.out.println(in.readObject()); System.out.println(in.readInt()); System.out.println(in.readObject()); in.close(); } }先看看这段代码的输出:
对比前边的SerialPerson的输出和这个类TransientKeyword的输出,这两个类的定义最大的区别就在于name的定义中是否使用了transient的关键字,对比它们的输出可以发现使用了transient和没使用的区别:
SerialPerson中针对Java对象的输出:
SerialPerson [name=Lang Yu, age=27]
TransientKeyword中针对Java对象的输出:
TransientKeyword [name=null, age=27]
注意看name属性的值:当Java序列化到一个文件中过后,通过反序列化读取出来的transient域的状态值并不是存入时候的值,而是null值,那么这一点可以说明transient域修饰过的域带有语义“不执行序列化”。总结一下这个关键字的内容:
transient的内容先讲到这里,等到读者理解了序列化的原理过后再回头来看看几个比较争议的问题。
3)特殊常量serialVersionUID
serialVersionUID常量的作用:Java在执行序列化的时候为了保持版本的兼容性,即JDK版本升级的时候在反序列化的过程仍然保证Java对象的唯一性。这个值一般有两种生成方式:
第一种:默认使用1L,比如使用下边这种定义:
private static final long serialVersionUID = 1L;
第二种:根据类名、接口名、成员方法以及属性来生成一个64位的哈希字段,其定义格式类似:
private static final long serialVersionUID = 3495065278062841972L;
一般在类似Eclipse和Netbeans的开发工具中,都可以自动生成这个值。
一个Java类若实现了Serializable接口,而且未定义serialVersionUID的情况会如何?出现这种情况的时候编译过程不会报错,但类似Eclipse的工具会提供相关的警告信息,而且Java在执行序列化的时候,系统会自动生成一个新的serialVersionUID的值。这个值的作用是为了实现Java跨版本的对象唯一性,使得Java在反序列化的过程不会因为JDK的版本冲突出现不一致的情况。serialVersionUID的取值是Java运行时环境根据内部细节自动生成的(上边提到的第二种),如果对类的源代码作了修改,再重新编译,新生成的类文件的serialVersionUID的取值有可能也会发生变化。其实serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,使用不同的Java编译器编译,有可能会导致不同的serialVersionUID,也有可能相同,为了提高serialVersionUID的独立性和确定性,建议在代码中显示定义serialVersionUID。显示地定义serialVersionUID有两种用途:
1)希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;
2)某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID;
到这里Java序列化的基本概念部分就结束了,接下来需要讲解的是Java序列化的核心原理,其内容包括一些需要深入理解的概念:算法、数据结构以及数据格式等。
2.序列化原理和算法——基础数据
i.序列化数据格式分析
Java序列化既然能把一个Java对象转化成一种特定的数据格式,那么本章就来分析Java内建序列化的基本算法和原理。在分析Java中的序列化的算法和原理的时候,先看一段代码:
package org.susan.java.serial; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; class SerialBase implements Serializable{ /** * */ private static final long serialVersionUID = 3495065278062841972L; public SerialBase() { System.out.println("Base Serial"); } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } private String name; private int age = 26; } public class SerialInner extends SerialBase implements Serializable { /** * */ private static final long serialVersionUID = 3208092597671621268L; public SerialInner() { System.out.println("Sub Serial"); } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } private transient String name; private int age; // 运行函数 public static void main(String args[]) throws Exception { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream( "inner.obj")); // 序列化对象 SerialInner person = new SerialInner(); person.setAge(27); person.setName("Lang Yu"); // 数据的序列化写入 out.writeBoolean(true); out.writeObject(person); out.writeInt(120); out.writeObject("[email protected]"); out.close(); // 反序列化对象 ObjectInputStream in = new ObjectInputStream(new FileInputStream( "inner.obj")); // 数据的反序列化读取 System.out.println(in.readBoolean()); System.out.println(in.readObject()); System.out.println(in.readInt()); System.out.println(in.readObject()); in.close(); } }这段代码的输出如下:
Base Serial
Sub Serial
true
org.susan.java.serial.SerialInner@74a138
120
[email protected]
但是这里我们不需要去关心这段代码的运行输出,我们集中精力在生成的文件inner.obj中。使用一个十六进制编辑器打开inner.obj文件,可得到类似下边的序列【注意颜色分段,只有黄色背景为TC*标记,文章后边会针对TC*标记单独说明】:
AC ED 00 05 77 01 01 73 72 00 21 6F 72 67 2E 73
75 73 61 6E 2E 6A 61 76 61 2E 73 65 72 69 61 6C
2E 53 65 72 69 61 6C 49 6E 6E 65 72 2C 85 6F 38
6A C6 F2 94 02 00 01 49 00 03 61 67 65 78 72 00
20 6F 72 67 2E 73 75 73 61 6E 2E 6A 61 76 61 2E
73 65 72 69 61 6C 2E 53 65 72 69 61 6C 42 61 73
65 30 80 F7 5A 4D BC D0 74 02 00 02 49 00 03 61
67 65 4C 00 04 6E 61 6D 65 74 00 12 4C 6A 61 76
61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 78 70
00 00 00 1A 70 00 00 00 1B 77 04 00 00 00 7874
00 17 73 69 6C 65 6E 74 62 61 6C 61 6E 63 65 79
68 40 31 32 36 2E 63 6F 6D
接下来根据代码一步一步解析上边的序列所表示的含义,希望读者在理解序列内容的基础上真正去理解JVM中的内建序列化算法,这也有助于读者去理解JVM序列化的顺序以及如何转化Java对象——实际上JAVA序列化内建机制把这些内容分成了两个主要部分【*:为了理解方便,下边在讲解时序列部分以“双位十六进制数(类似第一位AC)“为单位,即字节为单位——一个字节表示8个二进制位,用16进制表示则是2个16进制位,下文中带灰色背景的序列就是截取于上边的颜色段,而带“【】”符号中的序列就是按照序列中标记意义截取的——其划分粒度更加细致,两种方式各自拼凑起来都是上边的二进制全部序列。】:
1)开始部分:
AC ED 00 05:该序列为内建序列化的初始化过程,这部分数据在写入目标介质的时候生成,比如例子中使用的目标介质是文件,则该部分内容在下边代码部分生成:
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream( "inner.obj"));也就是说,不论系统是否调用了ObjectOutputStream的带write前缀的成员函数,JVM一旦发现代码中使用了ObjectOutputStream类,则意识到一点:“JVM将会向目标介质执行内建序列化操作”,于是JVM首先初始化序列化数据的格式。
/** * Version number that is written to the stream header. */ final static short STREAM_VERSION = 5;可以这样理解,这个版本号对应了当前Java使用的序列化机制的版本,目前的版本号是5,从注释可以知道这个值会写入流文件的文件头位置。
2)内容部分:
------------------writeBoolean------------------------
第一部分序列在执行了下边这段代码过后生成:
out.writeBoolean(true);77 01 01:上述 代码执行完毕后会生成一个基础数据boolean类型值为true的字节序列。
【01】(最后一个)标识了boolean类型的数据值,若为true则该序列是01,若为false则该序列为00——也就是说如果上边writeBoolean的参数为false则该序列就变成了77 01 00。
------------------writeObject------------------------
==>SerialInner元数据信息
从下边的序列开始,就是执行下边这句代码生成的内容:
out.writeObject(person);
73 72 00 21:该序列写入的是对象的基本信息:
【73】TC_OBJECT:该标记是一个声明标记,表示需要序列化的对象是一个新对象。
【72】TC_CLASSDESC:该标记一般紧跟TC_OBJECT标记,它表示接下来的二进制序列是当前对象的类描述信息【元数据】,接下来就写入当前对象的类描述信息到字节流。
【00 21】该描述信息表示当前对象所属类的类名(带包名的类全名)长度信息,这里值为0x21,转换成十进制其值为33,即——org.susan.java.serial.SerialInner(33字符长度);
【6F 72 67 2E 73 75 73 61 6E 2E 6A 61 76 61 2E 73 65 72 69 61 6C 2E 53 65 72 69 61 6C 49 6E 6E 65 72】
上述33个字节数据表示的就是当前对象所属类的类全名,即字符串:org.susan.java.serial.SerialInner
【2C 85 6F 38 6A C6 F2 94】
上述的8个字节数据表示的是当前对象所属类中定义的serialVersionUID的值,代码中的定义如下:
private static final long serialVersionUID = 3208092597671621268L;其值3208092597671621268转换成十六进制的值就是:2C 85 6F 38 6A C6 F2 94;需要注意的是如果一个类中没有定义该值系统会自动生成一个新的值,在二进制序列中追加在此处,因为serialVersionUID的类型是long类型的,所以它占用了8个字节,所以系统自动生成的时候也会自动创建一个long类型的数据【8个字节的二进制序列】。
【00 01】这两位表示当前这个对象中成员属性的数量,从其值可以知道当前对象(类型为org.susan.java.serial.SerialInner)的可序列化的成员属性有1个。注意:从org.susan.java.serial.SerialInner类定义中可知道该类应该有2个成员属性,为什么这里序列化过后统计出来的属性个数只有1个呢?
private transient String name; private int age;对比上边的定义可以知道一个事实:类定义的name属性使用了关键字transient,使用了此关键字的域具有”不会序列化“的语义,则在目标介质的数据中自然就没有该属性的信息,这里示例中的详细分析也可以让读者更加深刻地理解transient关键字在Java序列化中具有的作用。
【61 67 65】该值表示了成员属性的名称本身(61转化成Unicode的十进制值为97,即小写字母"a",67表示"g",65表示"e",三个数字连接在一起表示age,即属性age的定义名称。);
==>SerialBase元数据信息【父类】
78 72 00 20:该序列表示org.susan.java.serial.SerialInner类实例化的对象描述信息的结束,以及它的父类对象描述信息的开始。
private static final long serialVersionUID = 3495065278062841972L;
其值3495065278062841972转换成十六进制的值就是:30 80 F7 5A 4D BC D0 74;
02 00 02:该序列包含了父类对象是否支持序列化的信息,以及该类定义中成员属性的数量信息。
【02】同上,表示了父类对象是支持序列化的;
【00 02】同上,该值表示了父类对象中的成员属性的数量,先看看其代码定义:
private String name; private int age = 26;
【49 00 03 61 67 65】这一串请读者自己分析,和子类中的序列是一样的,它表示父类中age属性所有描述信息。
4C 00 04 6E 61 6D 65:接下来看看紧随其后的父类中另一个属性name的描述信息,注意:父类中的name属性是没有用transient关键字修饰的域。
【4C】此值转化成为字符"L",它表示了属性name的数据类型为String;
【00 04】该值表示了成员属性的名称长度——("name".length() == 4 );
【6E 61 6D 65】该值表示成员属性的名称:"name";
74 0012 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B:这段序列表示了从子类到父类的引用信息,有多少个引用就出现多少个类似序列,包括引用类型名长度以及类型名;
【74】TC_STRING:这个标记表示一个new String(新的字符串对象),这里需要用来记录存在的父类成员属性的引用——通过测试可以知道如果父类中没有String类型的属性name,或者name使用了transient进行修饰过,则上述的这一整段序列就不会出现;
【00 12】这个值表示的是对象签名的长度,也就是引用类型名的长度,转换成十进制数是18;("Ljava/lang/String;".length() == 18)
【4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B】这一段序列表示字符串:"Ljava/lang/String;",该字符串描述的是引用所属类型的类型签名,注意末尾的分号;
78 70:这两个二进制序列表示父类的类描述信息结束。
【78】TC_ENDBLOCKDATA:这个标记是对象块结束的标记,表示org.susan.java.serial.SerialBase的描述信息结束了;
【70】TC_NULL:这个标记表示该类再也没有任何”自定义超类“——虽然Java中所有的对象都是从Object继承,但因为Object本身并不支持序列化,所以当父类描述信息结束后,不会再有顶级父类Object(这里是org.susan.java.serial.SerialBase的直接父类)的序列化信息,同样也可以知道Java序列化会递归搜索继承树上的所有父类。
==> SerialInner&SerialBase实例数据信息
上边一直在讨论writeObject方法生成的序列化数据,这个方法生成的所有数据从上边提到的70(注意这个70是表示TC_NULL的70,而且是递归继承终止符。)开始是一个分水岭,前边介绍的是对象的类描述信息【元数据部分】,接下来将介绍对象的实例数据信息【数据部分】。
00 00 00 1A 70 00 00 00 1B
【00 00 00 1A】这一段数据表示父类org.susan.java.serial.SerialBase中age属性的值,换算成十进制数据值为26,对应下边的定义代码:
private int age = 26;
【70】TC_NULL:这个标记代表一个空引用(这里的TC_NULL只是一个空引用的数据,并不是递归继承终止符),它表示父类org.susan.java.serial.SerialBase中name属性的值,name未进行任何初始化,根据Java的语言规则,它的值为null,对应下边的定义代码:
private String name;
【00 00 00 1B】这段数据表示子类org.susan.java.serial.SerialInner中age属性的值,虽然该属性未进行初始化,但是在代码执行序列化之前,该属性拥有值27了,其对应代码如下:
// 序列化对象 SerialInner person = new SerialInner(); person.setAge(27); person.setName("Lang Yu");
在序列化对象创建过后,调用了对象的setAge方法进行过赋值。这里读者会有一个疑问:子类的name也赋值为"Lang Yu"了,这里为何没看见对应的序列呢?细心的读者会发现子类的name属性使用了transient修饰,其带有”不会序列化“的语义,所以不仅仅在类描述信息【元数据部分】,在实例数据信息【数据部分】中这部分内容也不会被序列化到目标介质中。
------------------writeInt------------------------
下边的序列为整数120部分的序列,其对应代码为:
out.writeInt(120);
77 04 00 00 00 78:这段信息描述了整数120的详细信息。
【77】TC_BLOCKDATA:可选的数据块,参考后边的章节就知道,所有基础类型数据的序列化都会使用数据块的结构;
【04】该值表示当前写入数据120值占用的字节数,因为是int类型的数据,所以这个120的数据应该占用4个字节;
【00 00 00 78】这段数据表示120的值;十六进制0x78转换成十进制就是120;
------------------writeObject------------------------
示例代码的最后一部分写入了一个常量字符串,所以下边这部分序列对应的代码为:
out.writeObject("[email protected]");
74 00 17 73 69 6C 65 6E 74 62 61 6C 61 6E 63 65 79 68 40 31 32 36 2E 63 6F 6D:这段信息描述了常量字符串的定义信息。
【74】TC_STRING:这个标记表示一个new String(新的字符串对象);
【00 17】这个值换成十进制为25,它表示了字符串"[email protected]"的长度;
【73 69 6C 65 6E 74 62 61 6C 61 6E 63 65 79 68 40 31 32 36 2E 63 6F 6D】这一段序列表示的就是字符串"[email protected]";
到这里,上边的示例就解析完了,通过这个例子是否理解Java内建序列化的基本用法了呢?简单总结下:
而Java内建序列化算法在序列化一个定义的Java对象的时候其顺序如下:
ii.基本数据类型序列化
前一个章节的例子演示了如何去阅读Java内建序列化生成的二进制文件,本章谈谈基本数据类型的序列化;Java中的基本数据类型有8种:byte、short、int、long、boolean、char、float、double,在讲解之前,先看看下边的代码:
package org.susan.java.serial; import java.io.FileOutputStream; import java.io.ObjectOutputStream; public class PrimarySerial { public static void main(String args[]) throws Exception { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream( "primary.obj")); // 针对Boolean数据的序列化 out.writeBoolean(true); out.writeBoolean(false); // 针对Short的序列化 out.writeShort(32); // 针对Int的序列化 out.writeInt(32); // 针对Long的序列化 out.writeLong(32L); // 针对Char的序列化 out.writeChar('a'); // 针对Byte的序列化 out.writeByte(18); // 针对Float的序列化 out.writeFloat(3.2f); // 针对Byte的序列化创建分割线:11 out.writeByte(17); // 针对Double的序列化 out.writeDouble(3.2); out.close(); } }
打开代码生成的二进制文件如下【注意颜色分段,只有黄色背景为TC*标记】:
AC ED 00 05 77 20 01 00 00 20 00 00 00 20 00 00
00 00 00 00 00 20 00 61 12 40 4C CC CD 11 40 09
99 99 99 99 99 9A
这里不再累赘分析上边的二进制文件,详细看看这段代码生成的二进制数据。
【77】TC_BLOCKDATA:可选的数据块,在Java序列化生成的二进制文件中,一段连续存储在一起的基础类型数据使用这个标记开始,前一个章节的例子有两处使用了TC_BLOCKDATA,其对应代码为:
out.writeBoolean(true); out.writeObject(person); out.writeInt(120);
因为在true和120两个基础数据之间多了一个对象的序列化,所以序列化中每次遇到独立的基础数据段就会使用【77】TC_BLOCKDATA进行标记。
【20】这个值表示可选数据块需要占用的存储空间的大小,0x20转换成十进制为32,简单计算一下:两个boolean数据各占用1个字节,short数据占用2个字节,int数据占用4个字节,long数据占用8个字节,char占用2个字节(Unicode格式的数据),byte数据占用1个字节,float数据占用4个字节,分割线11占用1个字节,double数据占用8个字节,合计:2*1 + 2 + 4 + 8 + 2 + 1 + 4 + 1 + 8 = 32,这个标记的含义就在此——注意基础数据在序列化的时候若是连续写入,则在写入每一个数据的时候不会出现新的TC_BLOCKDATA标记。
1)Boolean数据的写入
示例中的代码段为:
// 针对Boolean数据的序列化 out.writeBoolean(true); out.writeBoolean(false);
【01 00】这段数据表示写入的Boolean数据,01表示true,00表示false;Java中的boolean数据很特殊,它应该占用的空间是1个bit,即1/8个字节,而在序列化的时候写入的目标介质的时候出现了填充:高7位的数据用了0作填充。——可以这样理解:Java序列化到目标介质的时候,基础数据都使用了数据块(Data Block)进行存储,数据块中统计空间大小使用的最小单位是字节(上述的77 20中的20含义),针对基础数据而言,两个数据不可能共享字节,所以boolean数据虽然占用的空间是1个bit,但是在序列化的时候还是生成1个字节来存储。
2)Short、Int、Long数据的写入
示例中的代码段为:
// 针对Short的序列化 out.writeShort(32); // 针对Int的序列化 out.writeInt(32); // 针对Long的序列化 out.writeLong(32L);
00 20 00 00 00 20 00 00 00 00 00 00 00 20:这一段二进制序列表示了Short、Int、Long类型的数据。
【00 20】Short值32,占用了2个字节空间;
【00 00 00 20】Int值32,占用了4个字节空间;
【00 00 00 00 00 00 00 20】Long值32,占用了8个字节空间;
3)Char、Byte数据的写入
示例中的代码段为:
// 针对Char的序列化 out.writeChar('a'); // 针对Byte的序列化 out.writeByte(18);
00 61 12:这一段二进制序列表示了Char、Byte类型的数据。
【00 61】Char值,转换成十进制为97,字符'a',Java中的Char值是Unicode,占用2个字节空间;
【12】Byte值,转换成十进制为18,占用1个字节空间;
4)Float、Double数据的写入
示例中的代码段为:
// 针对Float的序列化 out.writeFloat(3.2f); // 针对Byte的序列化创建分割线:11 out.writeByte(17); // 针对Double的序列化 out.writeDouble(3.2);
40 4C CC CD 11 40 09 99 99 99 99 99 9A:这一段二进制序列表示Float、Double类型的数据,中间为了分析方便加入了分割字节【11】.
【40 4C CC CD】Float值,表示3.2f,占用4个字节空间;
【40 09 99 99 99 99 99 9A】Double值,表示3.2,占用8个字节空间;
有必要在这里简单讲讲Flout和Double的格式问题:浮点数的存储格式和整数不一样,一个float格式主要分成了3部分【Java中使用的浮点规范为IEEE765】:
符号位1位(31):最高位,表示float数据的正负,0为正,1为负;
幂指数8位(23-30):表示2进制的幂次;
有效位24位(0-22加上省略位):低23位,表示有效数字——二进制数的规格化表示中,小数点前的数为1(即二进制表示的最高位),所以一般省略,这样可以理解下边步骤中第4步为什么在高位追加1。
上边的Float数【40 4C CC CD】展开成2进制为:
01000000 01001100 11001100 11001101
将上边的格式转换:
0 10000000 10011001100110011001101
转换步骤如下:
结果相加约为3.2,从值可以发现著名的浮点数的精度问题,若要详细了解IEEE765规范可上网了解相关内容,这里不对其做详细介绍了。
iii.基本数据之“顺序”和“越界”
1)“顺序”问题
前边章节的例子演示了各种基本数据的序列化步骤,这里再看一段代码来了解基础数据类型有关的另外一个问题:
package org.susan.java.serial; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class PrimarySpec { public static void main(String args[]) throws Exception { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream( "primaryspec.obj")); // 针对Int的序列化 out.writeInt(80000); out.close(); // 反序列化对象 ObjectInputStream in = new ObjectInputStream(new FileInputStream( "primaryspec.obj")); // 数据的反序列化读取 System.out.println(in.readShort()); System.out.println(in.readShort()); in.close(); } }
上边这段代码的输出为:
1
14464
为什么会出现这种情况?其实原理很简单,Int值80000被序列化过后其生成的二进制序列为:【00 01 38 80】,因为基础数据在存储的时候使用的存储格式是连续的字节序列,从这个例子可以看出:基础数据在序列化成字节过后本身没有“类型”的概念,那么类型是怎么产生的呢?——类型是在反序列化的时候代码中的方法调用时产生的。上面的例子序列化了一个Int数据到目标介质,而在反序列化的时候却调用了readShort的方法,从字节长度上看程序不会有错,但是00 01 38 80被当成了两个Short值处理,00 01转换成Short值是1,38 80转换成Short值是14464。所以:在针对连续基础数据(同一个Data Block中)执行序列化和反序列化的时候,顺序很重要,如果序列化的写入顺序和反序列化的读取顺序不一致,将导致数据的逻辑错误,虽然程序本身是合法的,但这并不是开发人员的预期。
Java序列化基础数据的时候,虽然Boolean值只是占用了1个bit,即1/8个字节,其余的7位使用的是左填充,但在序列化的时候这种类型的值占用了1个字节,把上边的读取代码改成如下格式:
// 数据的反序列化读取 System.out.println(in.readBoolean()); System.out.println(in.readBoolean()); System.out.println(in.readBoolean()); System.out.println(in.readBoolean());
上边代码输出为:
false
true
true
true
发现问题了吗——对Boolean数据而言,只有序列【00】会被识别为false,即所以非0的值都会被反序列化成true。
2)“越界”问题
针对write前缀的函数,一般传入的数据都不会越界,但也有特殊情况:
package org.susan.java.serial; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class PrimaryRange { public static void main(String args[]) throws Exception{ ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream( "primaryrange.obj")); // 针对Short的序列化 out.writeShort(80000); // 针对Char的序列化 out.writeChar(70000); out.close(); // 反序列化对象 ObjectInputStream in = new ObjectInputStream(new FileInputStream( "primaryrange.obj")); // 数据的反序列化读取 System.out.println(in.readShort()); System.out.println(in.readChar()); in.close(); } }
上边这段代码的输出为:
14464
ᅰ
发现问题了么——Java中的Short和Char都是属于数字类型,但writeShort和writeChar均可以接受Int类型的参数,也就是说传入的参数范围会超过其值本身的范围。数值80000转换成十六进制的数应该是01 38 80,应该是一个3字节的数据,但是因为调用了writeShort函数,这个函数只能向目标介质中写入两字节,所以高位的01被截断了;同样的道理70000转换成十六进制格式为01 11 70,也是一个3字节的数据,高位被截断过后writeShort只写入了低位的两个字节变成了11 70。打开primaryrange.obj文件可发现其二进制序列为:
AC ED 00 05 77 04 38 80 11 70
iv.基本数据的包装类型
Java语言中针对所有的基本类型都有其对应的封装类型,接下来看看封装类型的序列化细节:
package org.susan.java.serial; import java.io.FileOutputStream; import java.io.ObjectOutputStream; public class WrapperSerial { public static void main(String[] args) throws Exception { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream( "wrapper.obj")); // 针对Boolean数据的序列化 out.writeBoolean(Boolean.TRUE); out.writeBoolean(Boolean.FALSE); // 针对Short的序列化 out.writeShort(Short.valueOf("32")); // 针对Int的序列化 out.writeInt(Integer.valueOf("32")); // 针对Long的序列化 out.writeLong(Long.valueOf("32")); // 针对Char的序列化 out.writeChar(Character.valueOf('a')); // 针对Byte的序列化 out.writeByte(Byte.valueOf("12")); // 针对Float的序列化 out.writeFloat(Float.valueOf("32")); // 针对Double的序列化 out.writeDouble(Double.valueOf("3.2")); // 封装类型的另外一种方式的序列化 // 针对Boolean数据的序列化 out.writeObject(Boolean.TRUE); out.writeObject(Boolean.FALSE); // 针对Short的序列化 out.writeObject(Short.valueOf("32")); // 针对Int的序列化 out.writeObject(Integer.valueOf("32")); // 针对Long的序列化 out.writeObject(Long.valueOf("32")); // 针对Char的序列化 out.writeObject(Character.valueOf('a')); // 针对Byte的序列化 out.writeObject(Byte.valueOf("12")); // 针对Float的序列化 out.writeObject(Float.valueOf("32")); // 针对Double的序列化 out.writeObject(Double.valueOf("3.2")); out.close(); } }
打开上边这段代码生成的二进制文件wrapper.obj,查看其二进制序列如下:
AC ED 00 0577 1F 01 00 00 20 00 00 00 20 00 00
00 00 00 00 00 20 00 61 0C 42 00 00 00 40 09 99
99 99 99 99 9A 73 72 00 11 6A 61 76 61 2E 6C 61
6E 67 2E 42 6F 6F 6C 65 61 6E CD 20 72 80 D5 9C
FA EE 02 00 01 5A 00 05 76 61 6C 75 65 78 70 01
73 71 00 7E 00 00 00 ……
上边仅仅截取了前半部分的内容,先简单总结下这段代码:
对比前一个章节的例子会发现,从第一行的第5个字节77开始到第三行的第5个字节9A这段序列基本是一模一样的,除了这里写入的Byte是12,而Float写入的是32,目的是提供读者一个可分析的数据空间,所以接下来仅仅分析上边截取的剩余部分的字节:
……73 72 00 11 6A 61 76 61 2E 6C 61
6E 67 2E 42 6F 6F 6C 65 61 6E CD 20 72 80 D5 9C
FA EE 02 00 01 5A 00 05 76 61 6C 75 65 78 70 01
73 71 00 7E 00 00 00……
上边的序列描述的是使用对象方式序列化两个Boolean对象生成的二进制序列,对应下边的代码:
// 针对Boolean数据的序列化 out.writeObject(Boolean.TRUE); out.writeObject(Boolean.FALSE);
73 72 00 11:这段序列是Boolean对象的类描述信息。
【73】TC_OBJECT:该标记是一个声明,表示序列化的是一个新对象;
【72】TC_CLASSDESC:紧接着描述该对象所属的类的类描述信息【元数据】;
【00 11】这个值转换成十进制的值为17,它表示新建对象所属类的类名长度("java.lang.Boolean".length() == 17);
【6A 61 76 61 2E 6C 61 6E 67 2E 42 6F 6F 6C 65 61 6E】这一段二进制序列就表示类名"java.lang.Boolean"这个17个字节的字符串;
【CD 20 72 80 D5 9C FA EE】这一段序列对应到Boolean中的serialVersionUID定义:
/** use serialVersionUID from JDK 1.0.2 for interoperability */ private static final long serialVersionUID = -3665804199014368530L;
上边这段定义来自于Boolean类的源代码,常量serialVersionUID的值为-3665804199014368530,转换成十六进制的值为:CD 20 72 80 D5 9C FA EE。
02 00 01:这一段二进制序列是该对象中的属性描述信息。
【02】该标记表示当前对象是支持序列化的;
【00 01】表示当前对象中的属性个数,该对象有1个属性,对应的定义代码为:
private final boolean value;
5A 00 05 76 61 6C 75 65:这一段二进制序列描述了value属性的相关信息。
【5A】该标记转换成十进制为90,其字符表示为‘Z’,它表示value字段的类型是boolean类型(关于类型编码后边会单独说明);
【00 05】该序列表示value属性的属性名的长度;("value".length() == 5)
【76 61 6C 75 65】该二进制序列表示的就是属性的名称字符串:"value";
78 70 01:这几个二进制序列表示Boolean.TRUE对象序列化结束部分。
【78】TC_ENDBLOCKDATA:该标记表示Boolean.TRUE对象的类描述信息【元数据部分】结束;
【70】TC_NULL:在递归序列化类描述信息【元数据部分】的时候,发现Boolean类没有超类,它的直接父类是Object,所以输出此标记;
【01】该标记表示值true,上边已经说过了Boolean类型的序列化字节结构,01表示true,00表示false;
到这里第一行代码writeBoolean(Boolean.TRUE)就结束了,接下来看看剩余部分的二进制序列:
73 71 00 7E 00 00 00:这一段二进制序列描述了下边这行代码执行过后的数据:
out.writeObject(Boolean.FALSE);
【73】TC_OBJECT:该标记是一个声明,表示序列化的将是一个新对象;
【71】TC_REFERENCE:该对象的类型,这里创建的类型为一个Boolean类的引用;
【00 7E 00 00】baseWireHandle:这个值是一个常量,它表示第一个赋值的句柄,它的定义代码如下:
/** * First wire handle to be assigned. */ final static int baseWireHandle = 0x7e0000;
【00】该标记表示值false;
到这里这个Boolean对象序列化过后生成的二进制序列就解析完成了,从上述的解析可以知道,基本数据包装类型使用对象方式序列化的时候,其序列化的规则和一个对象序列化的规则是一致的。
v.对象引用探索
前一个章节的末尾使用了常量TC_REFERENCE和baseWireHandle,这两个值究竟使用了什么方式来实现序列化数据中这么多对象的管理呢?先看看例子:
package org.susan.java.serial; import java.io.FileOutputStream; import java.io.ObjectOutputStream; public class ReferenceSerial { public static void main(String[] args) throws Exception { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream( "ref.obj")); // 检测对象 Integer first = new Integer("2"); Integer second = Integer.valueOf("2"); Boolean third = new Boolean("true"); // 第一次序列化first out.writeObject(first); out.writeObject(new Integer("3")); // 第二次序列化first,这里开始使用引用的方式 out.writeObject(first); out.writeObject(new Integer("4")); out.writeObject(new Integer("5")); // 第一次序列化second,对象方式 out.writeObject(second); out.writeObject(new Integer("6")); out.writeObject(new Integer("7")); // 第二次序列化second,引用方式 out.writeObject(second); // 第一次序列化third,对象方式 out.writeObject(third); out.writeObject(new Integer("6")); // 引用方式, 使用引用方式其值就不会变化 out.writeObject(second); out.writeObject(first); out.writeObject(third); out.close(); } }
接下来就需要仔细分析上述代码生成的二进制序列了:
…… 00 00 00 0273 71 00 7E 00 00 00 00 00 03 71 00
7E 00 02 73 71 00 7E 00 00 00 00 00 04 73 71 00
7E 00 00 00 00 00 05 73 71 00 7E 00 00 00 00 00
02 73 71 00 7E 00 00 00 00 00 06 73 71 00 7E 00
00 00 00 00 07 71 00 7E 00 06 ……
01 73 71 00 7E 00 00 00 00 00 06 71
00 7E 00 06 71 00 7E 00 02 71 00 7E 00 0A
分析序列之前把前边类描述信息省略(78 70为分界,提取其后边部分的二进制序列),对照下边的表格看看每一句writeObject究竟输出的是什么数据:
分析代码段 | 生成的二进制序列 | 实际数据值 | 序列化方式 |
out.writeObject(first); | ……00 00 00 02 |
2 | 对象方式 |
out.writeObject(new Integer("3")); |
73 71 00 7E 00 00 00 00 00 03 | 3 | 对象方式 |
out.writeObject(first); |
71 00 7E 00 02 | 2 | 引用方式 |
out.writeObject(new Integer("4")); |
73 71 00 7E 00 00 00 00 00 04 | 4 | 对象方式 |
out.writeObject(new Integer("5")); |
73 71 00 7E 00 00 00 00 00 05 | 5 | 对象方式 |
out.writeObject(second); |
73 71 00 7E 00 00 00 00 00 02 | 2 | 对象方式 |
out.writeObject(new Integer("6")); |
73 71 00 7E 00 00 00 00 00 06 | 6 | 对象方式 |
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); |
……01 | true | 对象方式 |
out.writeObject(new Integer("6")); |
73 71 00 7E 00 00 00 00 00 06 | 6 | 对象方式 |
out.writeObject(second); |
71 00 7E 00 06 | 2 | 引用方式 |
out.writeObject(first); |
71 00 7E 00 02 | 2 | 引用方式 |
out.writeObject(third); |
71 00 7E 00 0A | true | 引用方式 |
先仔细看看上边的表格,通过分析来理解序列化中TC_REFERENCE的详细用法,把上边的表格总结下【为了把Java语言中的引用和序列化数据中的引用区分,下边总结部分”Java引用“表示Java语言中的引用,”引用“表示使用了TC_REFERENCE的序列化数据中的引用】:
vi.基础类型做成员属性
上述标记中多次出现了类似71、72、78等各种具有语义的标记,在继续讲解之前先看看下边的内容。
TC_*标记表(位于接口java.io.ObjectStreamConstants,只列出了数据常量):
变量名称 | 十六进制值 | 十进制值 | 含义 |
baseWireHandle | 【00 7E 00 00】 | 8257536 |
该值一般位于TC_REFERENCE之后,为计数器的基数,它一般表示第一个赋值的句柄; |
STREAM_MAGIC | 【AC ED】 | -21267 | Java序列化数据中输出到目标文件的“魔数”段 |
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的字符串对象 |
十六进制值 | 对应的字符 | 字段的类型 |
42 | B | byte |
43 | C | char |
44 | D | double |
46 | F | float |
49 | I | int |
4A | J | long |
4C | L | 类或者接口类型 |
53 | S | short |
5A | Z | boolean |
5B | [ | 数组类型,array |
package org.susan.java.serial; import java.io.FileOutputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class MembersSerial implements Serializable{ /** * */ private static final long serialVersionUID = -5857955996190777817L; private byte mByte; private short height; private int age; private long birthday; private char gender; private boolean isChild; private float money; private double mDouble; private Integer mAge; public Integer getmAge() { return mAge; } public void setmAge(Integer mAge) { this.mAge = mAge; } public byte getmByte() { return mByte; } public void setmByte(byte mByte) { this.mByte = mByte; } public short getHeight() { return height; } public void setHeight(short height) { this.height = height; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public long getBirthday() { return birthday; } public void setBirthday(long birthday) { this.birthday = birthday; } public char getGender() { return gender; } public void setGender(char gender) { this.gender = gender; } public boolean isChild() { return isChild; } public void setChild(boolean isChild) { this.isChild = isChild; } public float getMoney() { return money; } public void setMoney(float money) { this.money = money; } public double getmDouble() { return mDouble; } public void setmDouble(double mDouble) { this.mDouble = mDouble; } public static void main(String args[]) throws Exception{ ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream( "members.obj")); // 创建对象MembersSerial MembersSerial member = new MembersSerial(); member.setAge(27); member.setBirthday(1000000000000L); member.setChild(false); member.setGender('M'); member.setHeight(Short.parseShort("179")); member.setmByte((byte)60); member.setmDouble(3.1415926); member.setMoney(2000.00f); // 设置包装对象 member.setmAge(Integer.valueOf("27")); // 序列化该对象 out.writeObject(member); out.close(); } }
先看看这段代码生成的二进制序列【黄色背景是TC标记,红色背景是类型标记】:
【属性定义】
…… 02 00 09 49 00 03 61 67 65 4A 00 08 62
69 72 74 68 64 61 79 43 00 06 67 65 6E 64 65 72
53 00 06 68 65 69 67 68 74 5A 00 07 69 73 43 68
69 6C 64 42 00 05 6D 42 79 74 65 44 00 07 6D 44
6F 75 62 6C 65 46 00 05 6D 6F 6E 65 79 4C 00 04
6D 41 67 65 74 00 13 4C 6A 61 76 61 2F 6C 61 6E
67 2F 49 6E 74 65 67 65 72 3B 78 70
【属性值】
00 00 00 1B
00 00 00 E8 D4 A5 10 00 00 4D 00 B3 00 3C 40 09
21 FB 4D 12 D8 4A 44 FA 00 00 …… 00 00 00 1B
上边截取的类定义信息中省略了类的定义信息,并且把属性部分的信息分成了两段,这里就不像上边的例子一一详细来分析这段序列中的每一个标记了,简单列举下,读者自己去分析其细节内容:
——元数据信息——
【02】标记当前序列化的对象是支持序列化的;
【00 09】当前对象中的属性个数;
【49 00 03 61 67 65】当前对象中的属性age的元数据信息,49是类型标记,表示当前属性是一个int类型;
【4A 00 08 62 69 72 74 68 64 61 79】当前对象中的属性birthday的元数据信息,4A是类型标记,表示当前属性是一个long类型;
【43 00 06 67 65 6E 64 65 72】当前对象中的属性gender的元数据信息,43是类型标记,表示当前属性是一个char类型;
【53 00 06 68 65 69 67 68 74】当前对象中的属性height的元数据信息,53是类型标记,表示当前属性是一个short类型;
【5A 00 07 69 73 43 68 69 6C 64】当前对象中的属性isChild的元数据信息,5A是类型标记,表示当前属性是一个boolean类型;
【42 00 05 6D 42 79 74 65】当前对象中的属性mByte的元数据信息,42是类型标记,表示当前属性是一个byte类型;
【44 00 07 6D 44 6F 75 62 6C 65】当前对象中的属性mDouble的元数据信息,44是类型标记,表示当前属性是一个double类型;
【46 00 05 6D 6F 6E 65 79】当前对象中的属性money的元数据信息,46是类型标记,表示一个float类型;
【4C 00 04 6D 41 67 65】当前对象中的属性mAge的元数据信息,4C是类型标记,表示当前属性是一个类或者接口;
【74 00 13 4C 6A 61 76 61 2F 6C 61 6E 67 2F 49 6E 74 65 67 65 72 3B】修饰mAge,因为它是一个Integer类型的对象,这里创建了一个TC_STRING引用来引用该对象;
【78 70】该对象的元数据的结束标记;
——数据信息——
【00 00 00 1B】表示int类型的数据27;
【00 00 00 E8 D4 A5 10 00】表示long类型的数据1,000,000,000,000(12个零);
【00 4D】表示char类型的数据'M';
【00 B3】表示short类型的数据179;
【00】表示boolean类型的数据false;
【3C】表示byte类型的数据60;
【40 09 21 FB 4D 12 D8 4A】表示double类型的数据3.1415926;
【44 FA 00 00】表示float类型的数据2000.00f;
这个位置省略了一段数据,这段数据是Integer对象的元数据信息,即Integer类以及其父类的类描述信息;
【00 00 00 1B】最后一个数据表示Integer类的对象中value属性的值;
细心的读者会发现:上边部分的二进制序列有一点点奇怪,那就是序列中的属性顺序——二进制序列中的属性的序列化顺序既不符合属性的定义顺序,也不符合属性使用顺序,那么这是为什么呢?答案很简单,这些成员描述的顺序会按照属性名的字典序进行排列(使用String的compareTo进行比较)。简单总结基本数据作为成员属性的序列化规则:
3.深入序列化规范
本章节大部分内容来源于对JVM的对象序列化规范的解读,有兴趣的读者可以直接查看该规范
【为了不误导读者,下边的”成员属性“和”字段“表示同一术语,只要读者可理解即可;而下边提到的字节流的英文并不是对应byte stream,而是stream,本来在写的时候准备直接使用”流“作为术语,但是考虑到理解的时候字节流对Java开发人员更加容易懂得,而且当使用ObjectOutputStream作为输出的时候其数据本身就是字节数据,所以采用了”字节流“为术语,其表示内容对应英文中的stream。因为我的翻译有限,只是在阅读基础之上加入了相关的描述,所以如果读者有无法理解的部分还是参考原规范为最佳,我只是为了写本文而参考,不保证翻译的精确性,但我会尽可能把语言整理得让读者容易理解。】:
http://docs.oracle.com/javase/7/docs/platform/serialization/spec/serialTOC.html
i.系统结构
1)序列化目标
JDK中设计序列化机制的目标如下:
2)写入对象流
Java语言中将一个Java对象转换成数据流的格式的过程代码如下:
// 将一个时间对象序列化成对象数据流 FileOutputStream f = new FileOutputStream("tmp"); ObjectOutput s = new ObjectOutputStream(f); s.writeObject("Today"); s.writeObject(new Date()); s.flush();
在Java语言中,序列化数据的基本规则如下:
看看下边的图来理解Output部分的接口和类的整体结构:
3)读取对象流
Java语言中从流数据中读取Java对象的过程如下:
// 从一个文件中反序列化成Java对象 FileInputStream in = new FileInputStream("tmp"); ObjectInputStream s = new ObjectInputStream(in); String today = (String)s.readObject(); Date date = (Date)s.readObject();
在Java语言中,反序列化数据的基本规则如下:
看看下边的图来理解Input部分的接口和类的整体结构:
4)对象流容器
Java中的对象序列化机制生产和消费的都是字节流数据【上边示例中的二进制序列】,这些字节流里面可能包含一个或多个Java基础类型数据以及Java对象数据——如果Java对象写入到流数据中引用了其他Java对象,这个字节流中同样也会描述这种关系。实际上Java对象充当了一个流数据容器,它提供了读取和写入字节流数据的接口,这两个接口就是ObjectOutput和ObjectInput:
如果一个Java对象要充当序列化中的流容器,它必须显示声明自己符合了JVM的序列化协议【通过实现java.io.Serializable接口】,这样的Java对象才能将自己的状态写入字节流【序列化】以及从字节流中读取Java对象状态重建该Java对象【反序列化】。JVM中定义了两套协议用于这种操作:
5)类中定义”可序列化“字段
在一个类中定义”可序列化“的字段有两种不同的办法;默认情况下——一个类里面只要字段的定义是非transient或者非静态的定义【不使用transient和static关键字】,那么这种字段就是可序列化的,使用Java的内建序列化进行处理。另外一种情况——定义可序列化的字段是在一个实现了Serializable接口的类中重写成员属性serialPersistentFields,这个属性的类型必须是一个ObjectStreamField的数组【ObjectStreamField[]】,这个数组枚举了所有需要序列化的字段名称和值的集合,而且这个属性的修饰符必须是固定的,其格式如下:
class List implements Serializable { List next; private static final ObjectStreamField[] serialPersistentFields = {new ObjectStreamField("next", List.class)}; }
如果属性serialPersestentFields的修饰符不匹配、或者类型不对、或者值为null,则这种定义方式无效。看个完整的例子:
package org.susan.java.serial; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.ObjectStreamField; import java.io.Serializable; public class SerialFields implements Serializable { /** * */ private static final long serialVersionUID = -8928285570197854374L; private int age = 27; private String name; private Integer iAge = 27; private static final ObjectStreamField[] serialPersistentFields = { new ObjectStreamField("age", int.class), new ObjectStreamField("iAge", Integer.class) }; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getiAge() { return iAge; } public void setiAge(Integer iAge) { this.iAge = iAge; } @Override public String toString() { return "SerialFields [age=" + age + ", name=" + name + ", iAge=" + iAge + "]"; } // 运行函数 public static void main(String args[]) throws Exception { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream( "serialfields.obj")); // 序列化对象 SerialFields person = new SerialFields(); person.setAge(27); person.setName("Lang Yu"); // 数据的序列化写入 out.writeObject(person); // writeExternal 自动执行 out.close(); // 反序列化对象 ObjectInputStream in = new ObjectInputStream(new FileInputStream( "serialfields.obj")); // 数据的反序列化读取 SerialFields target = (SerialFields) in.readObject(); System.out.println(target); in.close(); } }
上边代码的输出为:
SerialFields [age=27, name=null, iAge=27]
注意输出的name属性的值为null,因为在这个类里面有下边定义:
private static final ObjectStreamField[] serialPersistentFields = { new ObjectStreamField("age", int.class), new ObjectStreamField("iAge", Integer.class) };
上边的定义中声明属性serialPersistentFields,它定义了可序列化的成员属性表,这种情况下默认字段中的”可序列化“语义会无效,而name属性并没有在这个定义中,所以输出的数据中name属性的值为null。关于serialPersistentFields的用法,还有一种是使用mapping机制:定义的成员属性的名称不一定要在序列化类中存在,如果不存在的情况,可定义mapping,这一点本文就不详细说明了。
6)可序列化属性的文档化
Java的可序列化的文档化操作要使用到《Java基础知识【上】》中提到过的几个注释标记:@serial、@serialField、@serialData。
7)操作类定义中的可序列化字段
Java序列化机制提供了在字节流中操作序列化字段的两种方法:
8)ObjectOutput接口
java.io.ObjectOutput接口的源代码定义如下:
package java.io; public interface ObjectOutput extends DataOutput, AutoCloseable { public void writeObject(Object obj) throws IOException; public void write(int b) throws IOException; public void write(byte b[]) throws IOException; public void write(byte b[], int off, int len) throws IOException; public void flush() throws IOException; public void close() throws IOException; }
9)ObjectInput接口
java.io.ObjectInput接口的源代码定义如下
package java.io; public interface ObjectInput extends DataInput, AutoCloseable { public Object readObject() throws ClassNotFoundException, IOException; public int read() throws IOException; public int read(byte b[]) throws IOException; public int read(byte b[], int off, int len) throws IOException; public long skip(long n) throws IOException; public int available() throws IOException; public void close() throws IOException; }
ii.对象输出结构【序列化】
从上边的例子可以知道,Java中对象的序列化需要使用ObjectOutputStream类,该类可以维护字节流中已经序列化过的对象的状态,它的方法可控制各种不同的对象之间的结构——包括继承和组合。
该类有一个单参数的构造函数,它的参数类型为OutputStream,其构造函数的签名如下:
public ObjectOutputStream(OutputStream out) throws IOException
这个构造函数在构造这个对象的时候会先调用writeStreamHeader()在序列化的目标介质中写入魔数和序列化的版本,在反序列化的时候,系统会调用readStreamHeader()方法先验证魔数和序列化的版本是否匹配,如果不匹配则抛出序列化的异常。如果JVM中安装了安全管理器,当构造函数被子类的构造函数直接或者间调用时,子类若重写了putFields和writeUnshared方法,这个构造函数还会检查”enableSubclassImplementation“ SerializablePermission以确定代码的执行权限。
这个类中最核心的方法是writeObject方法,其函数签名如下:
1)writeObject
public final void writeObject(Object obj) throws IOException
前边的示例中在分析序列化生成的二进制序列的时候多次提到这个方法,这个方法在序列化一个Java对象的时候会遵循下边的规则【后边源码分析会详细说明】:
2)writeUnshared
public void writeUnshared(Object obj) throws IOException
这个类中有一个writeUnshared方法,这个方法会把”非共享“的对象写入到字节流,而且每次写入对象时都把对象当做一个新对象处理;
若使用了writeUnshared方法序列化了当前Java对象,在反序列化的时候它自己并不能保证对象的唯一引用,它允许在字节流里面多次定义单个Java对象,所以多次调用ObjectInputStream.readUnshared方法并不会产生冲突。
3)defaultWriteObject
public void defaultWriteObject() throws IOException
defaultWriteObject方法实现了针对当前对象的默认序列化机制,但是这个方法只能从writeObject方法中调用,它会将一个类中定义的所有可序列化的所有字段写入到字节流,如果不是从writeObject方法中调用的该方法,则会抛出NotActiveException异常。
4)putFields、writeFields
public ObjectOutputStream.PutField putFields() throws IOException public void writeFields() throws IOException
putFields方法:调用者会设置字节流中所有可序列化的字段的值,该方法将会返回ObjectOutputStream.PutField类型的对象,这些字段可以按照任何顺序设置,所有的字段数据设置过后,必须调用writeFields方法按照设置时的顺序将字段的值按固定的顺序写入字节流。如果一个字段的值没有设置,它对应的类型的默认值会写入到字节流,例如一个字段的类型是Int,它没有被设置过,则一个4字节的Int整数0将会写入到字节流。这个方法只能在writeObject方法内部调用,如果针对当前字段已经调用过defaultWriteObject方法了,writeFields方法就不能再调用——一次都不可以,仅仅在方法writeFields调用过后才能将其他数据写入到字节流。
5)reset
public void reset() throws IOException
reset方法将会清除字节流的状态,将字节流还原到刚刚开始构造时的对象,reset方法执行过后,它会将已经写入到字节流的Java对象的状态全部清空然后重置该字节流。当前字节流的写入点会被标记为reset状态,在使用ObjectInputStream进行反序列化的时候,当它发现字节流中存在reset标记,它会在同样的位置执行重置操作。先前已经被序列化写入到字节流的Java对象将不会被系统记住,也意味着前边写入字节流的数据在此处会被清空,但是这些Java对象会随后被重新写入字节流,当对象的内容需要重新发送的时候这个功能就可以派上用场了。但是reset方法不能够在Java对象正在被序列化的时候调用,这种情况会抛出IOException异常。
6)writeClassDescriptor
protected void writeClassDescriptor(ObjectStreamClass desc) throws IOException
从JDK 1.3开始,当一个ObjectStreamClass类型的对象需要被序列化的时候,系统会调用writeClassDescriptor方法。ObjectStreamClass对象实际上是一个Java对象的类描述信息对象,它提供了当前Java对象的类描述信息【元数据】,调用了writeClassDescriptor方法过后,系统会将Java对象的类描述信息写入到字节流中。如果writeClassDescriptor方法被重写过后,在使用ObjectInputStream反序列化Java对象的时候,类中中的readClassDescriptor也应该同时被重写,默认情况下,writeClassDescriptor方法会有固定的字节语法格式来写入类描述信息。注意这个方法只能在ObjectOutputStream没有使用旧的序列化流格式的时候调用,如果序列化的字节流格式使用的是旧的协议(ObjectStreamConstants.PROTOCOL_VERSION_1),这个类描述信息只能使用内部的方式写入字节流,这种情况下它不能被重写,也不可以被定制。
7)annotateClass
protected void annotateClass(Class<?> cl) throws IOException
当一个Class类型的对象被序列化的时候,在它本身的类描述信息【元数据】写入到字节流之后,annotateClass方法会被调用。子类也许会继承或者重写这个方法,将一些和当前Class类型的对象相关的额外信息写入到字节流。这些信息在反序列化的时候会被ObjectInputStream子类中的resolveClass方法读取。
8)replaceObject
protected Object replaceObject(Object obj) throws IOException
一个ObjectOutputStream子类可以实现方法replaceObject,这个方法在Java对象序列化的时候用于监控或者替换Java对象。在调用writeObject方法将第一个Java对象替换之前,必须通过调用enableReplaceObject方法显示声明——”启用对象替换“。一旦调用了该方法过后,在第一次序列化每个Java对象时,会优先调用replaceObject方法。注:replaceObject方法在遇到特别的类(Class和ObjectStreamClass)时不会被调用。子类的实现将会返回一个替代对象,它将替代原始对象执行序列化操作,这个替代对象必须是可序列化的,而所有字节流中的指向原始对象的引用也会被替换使其指向替代对象。
当Java对象被替换过后,它的子类必须保证引用指向的存储对象中的字段和替代对象中的字段是匹配的【主要会检测对象本身的类型以及对象中成员属性的类型】,或者替代对象中的字段相关信息是在序列化的时候生成的。如果一个对象的类型不属于其子类型,也不属于成员属性的类型,同样不属于数组元素等【类型不匹配】,这个对象在反序列化的时候会抛出ClassCastException的异常,同样它对应的引用不会被存储。
9)enableReplaceObject
protected boolean enableReplaceObject(boolean enable) throws SecurityException
这个方法调用的前提是充分相信ObjectOutputStream类型的子类,它启用了序列化中的”对象替换“的功能,在没有调用enableReplaceObject(true)之前”对象替换“的功能在序列化中是禁用的,在执行了enableReplaceObject(false)之后,序列化中的”对象替换“功能又会被禁用。enableReplaceObject方法将会检测字节流中请求的替代对象是否可信任对象。为了保证私有对象的状态是非故意暴露的【破换了封装】,仅仅只有可信任的子类能调用replaceObject方法,这些可信任的子类是属于安全域中受保护的对象,系统授予了替代对象的可序列化权限。
如果ObjectOutputStream的子类并不属于系统域中的一部分,SerializablePermission "enableSubstitution"权限将会被添加到安全策略文件中;在反序列化的时候,ObjectInputStream的子类对象在受保护域中若没有"enableSubstitution"权限,但它却调用了enableReplaceObject方法,则会抛出AccessControlException的异常。
10)writeStreamHeader
protected void writeStreamHeader() throws IOException
该方法为每次序列化到目标介质第一个调用的方法,它会将魔数和序列化的版本写入到字节流。这些信息将会在反序列化的时候被ObjectInputStream类中的readStreamHeader方法读取,它的子类需要实现这些方法并且检查魔数和序列化的版本数据是否字节流的唯一格式。
11)flush、drain
public void flush() throws IOException protected void drain() throws IOException
flush方法调用过后,缓冲区中的数据将会被写入到字节流,然后会清空该缓冲区。而drain方法和flush方法唯一的不同就是它只会清空ObjectOutputStream的缓冲区,而不会强制将缓冲区数据写入字节流。
综上所述,针对基础数据的序列化而言,所有的write*写入方法在写入值时,其值都会使用DataOutputStream转换成标准的字节流格式。这些字节会在缓冲区中使用Data-Block【数据块】的方式记录下来以便它执行反序列化操作。这种情况下处理基础数据的时候,会跳过类的版本检测,同样它允许解析字节流的时候不去调用类的特殊方法,也就是说这种类型的数据不会使用“对象方式”执行序列化和反序列化操作。
所有重写了序列化的实现中,ObjectOutputStream类的子类都必须调用它的protected修饰的无参构造函数,这个地方会调用安全管理器检测执行代码是否拥有SerializablePermission “enableSubclassImplementation”的权限,也就是说只有受信任的子类可重写它对应的实现。这个构造方法它不会为ObjectOutputStream的私有数据分配任何空间,但它会设置标记以告诉它的final方法writeObject在调用的时候应该调用writeObjectOverride方法,ObjectOutputStream类中所有的非final的方法,它都能够直接被子类重写。
iii.对象输入结构【反序列化】
前边一个章节讲解了Java中序列化的核心类ObjectOutputStream,这个章节来解析Java反序列化使用的ObjectInputStream类,这个类可以从字节流中恢复Java对象的状态。
这个类也有一个单参数的构造函数,此参数的类型为InputStream,其构造函数的签名如下:
public ObjectInputStream(InputStream in) throws IOException
这个构造函数会调用readStreamHeader()方法读取魔数信息以及序列化版本信息,并且检测通过ObjectOutputStream写入的魔数信息以及序列化版本是否匹配。若已经安装了安全管理器,如果子类重写了readFields和readUnshared方法,则这个构造函数同样会调用安全管理器以确认执行代码是否包含了“enableSubclassImplementation” SerializablePermission的权限。
1)readObject
public final Object readObject() throws IOException, ClassNotFoundException
和ObjectOutputStream类相对应的,ObjectInputStream类具有一个核心的反序列化的方法readObject,其方法会重构字节流中的Java对象,其规则如下:
在ObjectInputStream中所有读取基础类型的方法将会从字节流的Data Block【数据块】序列段中读取数据,在读取字节流中的基础数据的时候如果遇到接下来的数据项是一个Java对象,则这个读取方法会返回-1或者抛出一个EOFException异常。基础数据的读取会使用DataInputStream类从Data Block【数据块】中读取。在反序列化中如果有异常信息抛出,则标志着在读取基础数据流的时候出现了错误,一旦出现异常,则基础数据流会标记为“未知的”和“不安全”的。
当ObjectInputStream类在读取字节流的时候,一旦遇到了reset标记,则数据流中所有的状态将会无效,同时它会清空“已知对象”【known objects】的集合;一旦在字节流中遇到了exception标记,则这个异常信息会被读取,一个新的WriteAbortedException将会抛出,当前的字节流的上下文也会被重置。
2)readUnshared
public Object readUnshared() throws IOException, ClassNotFoundException
该类中的readUnshared方法用来从数据流中读取"unshared"的对象,这个方法和readObject方法是相同的,但是若反序列化的对象是通过原始对象调用readUnshared方法生成的,而在第二次调用readUnshared方法的时候仍然会恢复一个新的对象,而readObject在第二次调用的时候不会恢复一个新的对象,而是重建一个Java引用:
通过readUnshared方法反序列化一个对象的时候,它会使得和对象关联的引用Handle无效。需要注意的是通过调用readUnshared不能保证对象引用的唯一性;也许反序列化的对象中定义了readResolve方法,使得这个对象对其他内容可见【破坏了封装】,或者readUnshared将会返回一个Class类型的对象,又或者返回一个Enum常量。如果这个反序列化对象中定义了readResolve方法而且这个调用该方法返回了一个数组(array),接着readUnshared方法将会返回这个数组的一个影子拷贝(副本shallow clone);这样能够保证返回的数组对象是唯一的,即使基础数据字节流是可操作的,这个对象不能让ObjectInputStream类第二次调用readObject方法或者调用readUnshared来获得。
3)defaultReadObject
public void defaultReadObject() throws IOException, ClassNotFoundException
该类中的defaultReadObject方法用来从字节流中读取对象的字段值,它可以从字节流中按照定义对象的类描述符以及定义的顺序读取字段的名称和类型信息。这些值会通过匹配当前类的字段名称来赋予,如果当前这个对象中的某个字段并没有在字节流中出现,则这些字段会使用类中定义的默认值,如果这个值出现在字节流中,但是并不属于对象,则放弃读取。该情况只适用于下边的情况——最新版本的类中拥有额外的字段信息,而这些信息没有在老版本的类中出现过。这个defaultReadObject方法只能从readObject方法的内部进行调用,如果在其他地方调用该方法,会抛出NotActiveException异常。
4)readFields
public ObjectInputStream.GetField readFields() throws IOException, ClassNotFoundException
该方法会从字节流中读取可序列化的成员属性的值,同样使得这些字段在GetField类中是合法的。同样的,readFields方法也只能从可序列化的类中定义的readObject方法的内部调用,如果已经调用过defaultReadObject方法了那么这个方法就不再调用了。GetField对象使用当前对象的ObjectStreamClass信息来验证字段的相关信息,GetField对象通过调用readFields方法返回,而且它只在调用类中的readObject方法的时候有效。这些字段的值可以使用任意顺序进行读取,在readFields方法调用过后,才能从字节流中读取额外的数据。
5)registerValidation
public void registerValidation(ObjectInputValidation obj, int prio) throws NotActiveException, InvalidObjectException
在原始调用者调用readObject返回对象之前,而这个对象的状态已经被恢复之后,可以通过调用registerValidation方法申请并且注册回调函数。这些验证回调函数的顺序可以通过第二个参数优先级进行控制,优先级高的回调函数将在优先级低的回调函数之前调用。若当前对象需要被验证,它必须实现ObjectInputValidation接口并且实现接口中的validateObject方法。注册验证回调函数有一个限制:只有在可序列化的类中调用readObject方法的时候调用该方法可注册成功,否则会抛出一个NotActiveException异常。如果通过registerValidation注册的回调函数为null,则会抛出一个InvalidObjectException异常。
6)readClassDescriptor
protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException
从JDK 1.3的版本开始,调用readClassDescriptor方法可以从字节流中读取所有的类描述信息,如果反序列化的时候发现字节流的下一项包含了类描述信息,则可调用readClassDescriptor方法。而ObjectInputStream类的子类可能重写这个方法来读取非标准格式的类描述信息【元数据】,这些非标准格式的类描述信息是通过ObjectOutputStream中重写过的writeClassDescriptor方法生成的。默认情况下,这个方法会读取标准格式的类描述信息【Java中定义的格式】——从这里可以知道,如果重写了ObjectOutputStream中的writeClassDescriptor方法生成了非标准格式的类描述信息,则必须重写ObjectInputStream中的readClassDescriptor方法以读取这种格式的数据。
7)resolveClass
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException
如果字节流中需要反序列化的对象是一个Class类型,则在该对象被反序列化的时候调用resolveClass方法,子类有可能继承ObjectInputStream类并且重写resolveClass方法,重写过的方法可以读取和类相关的其他信息,这些信息是被重写过的ObjectOutputStream的子类写入到字节流的。resolveClass方法调用过后,这个方法查找类的名称以及它的serialVersionUID来生成需要反序列化的Class类型的对象,该方法会返回此对象。如果需要返回一个Class类型的对象,必须要使用Java的ClassLoader去载入该类,默认的实现中,使用的ClassLoader采取就近原则,哪个对象调用了readObject方法,则使用的ClassLoader就和该对象保持一致。如果通过名称以及serialVersionUID找不到该类的时候,抛出ClassNotFoundException的异常信息。在JDK1.1.6之前,resolveClass方法调用过后必须返回和字节流中类名一致的类全名,为了保证在发布过程重命名包以后能够适配,resolveClass方法在JDK 1.1.6以及之后的版本只需要返回一个带有基本类名和serialVersionUID的类即可。
8)resolveObject
protected Object resolveObject(Object obj) throws IOException
在反序列化过程中,resolveObject方法会被可信任的子类使用来监控或者替换某个对象。在反序列化过程中调用readObject方法去处理第一个对象之前,"Resolving Objects"的功能必须通过调用enableResolveObject方法显示开启。一旦启用了"Resolving Objects“功能,之前第一次调用readObject返回的所有可序列化的对象都会调用一次resolveObject。注:resolveObject方法的调用不能针对特殊的一些类型Class、ObjectStreamClass、String以及数组。子类若实现了resolveObject方法在调用过后会返回一个替代对象替换掉原始对象,而且返回的替代对象的类型必须可以赋值给原始对象的所有引用【类型匹配】,否则会抛出一个ClassCastException异常。这一步赋值的所有操作都需要执行类型检查,而字节流中指向原始对象的引用也会被替代对象的引用替换掉。
9)enableResolveObject
protected boolean enableResolveObject(boolean enable) throws SecurityException
enableResolveObject方法只能被ObjectInputStream类的可信任子类调用,一旦调用了该方法就会在反序列化过程中启用监控或者替换对象的功能。在调用enableResolveObject(true)之前,”Resolving Objects“的功能是禁用的,如果之后又调用了enableResolveObject(false)方法,则该功能就会再次禁用。enableResolveObject方法在反序列化过程调用的时候会检查代码是否有对象替换的执行权限,为了保证对象的状态中的私有属性不会暴露,仅仅只有可信任的字节流数据才可调用resolveObject方法。可信任的类是这样的类:要么在ClassLoader中的类是null,又或者在一个安全保护域中为当前这个类赋予了权限执行替换操作。
如果ObjectInputStream的子类并不是系统域的一部分,则针对ObjectInputStream的子类调用enableResolveObject方法的权限将会添加一行代码到安全策略文件中。添加的权限为SerializablePermission,内容为”enableSubstitution“,如果ObjectStreamClass子类的保护域中并没有”enableSubstitution“的权限,则会抛出AccessControlException的异常。
10)readStreamHeader
protected void readStreamHeader() throws IOException, StreamCorruptedException
调用该方法会读取序列化文件中的魔数信息和序列化字节流的版本信息,并且验证该信息是否匹配,如果不匹配则抛出StreamCorruptedMismatch异常。
为了重写反序列化的实现,ObjectInputStream的子类应该调用ObjectInputStream的protected修饰的无参构造函数,在这个无参构造函数会执行针对SerializablePermission "enableSubclassImplementation"的安全检查以保证只有受信任的类可以重写默认的实现。这个构造函数不会为任何ObjectInputStream的私有数据分配空间,它只是设置了一个标记告诉readObject方法去调用readObjectOverride方法。除开这个final的readObject方法之外,ObjectInputStream中其他所有的方法都不是final的,可以直接被子类重写。
iv.类描述符
前边几个章节多次提及了ObjectStreamClass类,那么这个类究竟是用来做什么的呢?ObjectStreamClass类提供了字节流中和类相关的信息,一般是某个类的描述信息等,类描述符提供了一个类的唯一的类全名【包含包名】和一个序列化的版本UID【serialVersionUID】,这些信息主要用来标识写入字节流和读取字节流中类的唯一原始版本。
1)ObjectStreamClass中的常用API:
lookup
public static ObjectStreamClass lookup(Class<?> cl)
该方法会解析虚拟机中某个特定的类并且返回一个ObjectStreamClass对象,如果这个类定义了serialVersionUID,则直接从该类里面读取该字段的值,如果serialVersionUID字段在这个被解析的类中没有定义,则虚拟机会按照这个类本身的定义来计算一个新的serialVersionUID的值。如果解析的类不支持序列化或者外部化,则该方法返回null。
lookupAny
public static ObjectStreamClass lookupAny(Class<?> cl)
该方法会解析虚拟机中所有的类并且返回一个ObjectStreamClass对象,这些类包括没有实现Serializable接口的类,如果一个类没有实现Serializable接口,则它对serialVersionUID属性的存在与否没有要求,这种情况下解析该类的serialVersionUID的值为0L。
getName
public String getName()
该方法返回当前解析类的类名,这个类名和Class.getName方法使用的类名格式一样。
forClass
public Class<?> forClass()
一旦调用ObjectInputStream的resolveClass方法在本地虚拟机中找到了一个Class则返回该Class,否则返回null。
getFields
public ObjectStreamField[] getFields()
该方法返回一个ObjectStreamField的数组,用来描述一个类中所有可序列化的字段的集合。
getSerialVersionUID
public long getSerialVersionUID()
该方法返回一个类中定义的serialVersionUID属性,如果系统中的可序列化的类未定义该属性,这个值将会根据定义的类的类名、接口、方法、字段使用算法Secure Hash Algorithm(SHA)计算一个新的。
toString——这个方法这里就不解释了。
接下来看个例子理解一下ObjectStreamClass中的API:
package org.susan.java.serial; import java.io.ObjectStreamClass; import java.io.ObjectStreamField; public class ObjectSCTest { private String name = ""; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } private int age = 12; // 运行函数 public static void main(String args[]) throws Exception { // 分别分析一个序列化类和非序列化的类 ObjectStreamClass scNonSerial = ObjectStreamClass.lookupAny(ObjectSCTest.class); ObjectStreamClass scSerial = ObjectStreamClass.lookup(SerialPerson.class); // 打印类名 System.out.println(scNonSerial.getName()); System.out.println(scSerial.getName()); // 打印所有信息,自动调用toString() System.out.println(scNonSerial); System.out.println(scSerial); // 返回字段描述 ObjectStreamField[] fields = scSerial.getFields(); for( ObjectStreamField field : fields){ System.out.println(field); } } }
上边的代码输出的值为:
org.susan.java.serial.ObjectSCTest
org.susan.java.serial.SerialPerson
org.susan.java.serial.ObjectSCTest: static final long serialVersionUID = 0L;
org.susan.java.serial.SerialPerson: static final long serialVersionUID = 1041712221752728541L;
I age
Ljava/lang/String; name
请读者结合输出理解该类的核心API的基本用法。
2)动态代理类描述符:
ObjectStreamClass不仅仅可以分析Java中直接类,同样可以分析字节流中存储的Java动态代理类(通过调用java.long.reflect.Proxy类的getProxyClass类)。对一个Java的动态代理类而言,它自己没有可序列化的成员属性,而且它的serialVersionUID的值为0L,换句话说若上述的lookup方法的实参传入的是一个动态代理类则会有下边的结果:
3)序列化形成
一个ObjectStreamClass类的实例的序列化形成过程依赖于传入的类是否为一个可序列化类【实现Serializable接口】、外部化类【实现Externalizable接口】或者Java的动态代理类。
如果字节流中一个ObjectStreamClass的实例没有描述一个Java的动态代理类,则它的基本形成格式为:类名、serialVersionUID、标记、成员属性的数量,而基于类的本身,它又包含下边的规则:
如果ObjectStreamClass实例描述的是一个动态代理类(通过java.lang.reflect.Proxy中的isProxyClass进行检测),它将写入该动态代理类实现的接口数量,然后写入接口名称,接口的顺序根据动态代理类代理的Class调用getInterfaces方法返回的接口列表排序一致。动态代理类和非动态代理类使用的二进制标记不一样,动态代理类使用TC_PROXYCLASSDESC,而非动态代理类使用TC_CLASSDESC【前边已经讲解过】。
4)ObjectStreamField中的API:
ObjectStreamField对象用来描述一个类中定义的可序列化字段定义信息以及字节流中存在字段的数据信息,它的构造函数如下:
public ObjectStreamField(String name, Class<?> type) public ObjectStreamField(String name, Class<?> type, boolean unshared)
参数name表示字段名,type表示该字段的类型,boolean表示当前字段是否以一个“unshared”对象执行默认的序列化和反序列化操作。
getName
public String getName()
返回可序列化的成员属性的属性名,也就是字段名。
getType
public Class<?> getType()
返回可序列化的字段的字段类型。
getTypeString
public String getTypeString()
返回字段类型的类型签名,如果该字段是基础数据类型int、long、char、short、boolean、byte、double、float,则该方法返回null。
getTypeCode
public char getTypeCode()
返回字段类型的类型编码,其编码对应关系如:
B——byte,C——char,D——double,F——float,I——int,J——long,L——非数组类对象类型,S——short,Z——boolean,[——数组类型【上一个章节的表格里有】
isPrimitive
public boolean isPrimitive()
如果字段为基础数据类型返回true,否则返回false。
isUnshared
public boolean isUnshared()
如果字段以“unshared”的对象写入到字节流则返回true,否则返回false;
5)字节流唯一标识
在Java序列化的字节流中,所有可序列化的类版本号都依靠字段serialVersionUID进行标识,注意这个字段的名称和修饰符:
private static final long serialVersionUID = 3487495895819393L;
一个类的字节流唯一标识符是通过类名、所有接口名、方法、成员属性生成的一个64位哈希值,这个值必须在所有版本的类中第一个定义,它也可以在原始的类中定义,但是不是必须的。如果一个SUID没有在类中定义,系统为这个类自动生成一个。对Java的动态代理类和Enum枚举常量类,serialVersionUID的值永远维持0L,所有数组类型不需要显示定义serialVersionUID值,因为它有默认计算出来的值。
*:强烈建议在可序列化的类中显示定义serialVersionUID值,因为系统生成的值是根据类信息计算的,而每次编译有可能改变,若使用系统生成的值有可能在序列化和反序列化过程产生不必要的冲突。
针对外部化的类初始版本号在将来是允许扩展的,而readExternal方法在将来能够读取所有writeExternal的版本号。
serialVersionUID在字节流中使用签名的方式反映了类的定义,字节流中签名的计算使用了NIST组织的SHA算法。前两位的32-bit数字用来处理64位哈希值,使用java.lang.DataOutputStream用来将基础数据转换成字节流的顺序,输入字节流的值是在JVM的类规范中定义的。类的修饰符包括ACC_PUBLIC、ACC_FINAL、ACC_INTERFACE和ACC_ABSTRACT标记,其他的标记会被忽略,因为其他标记不影响serialVersionUID值的生成;同样的针对字段的修饰符包括ACC_PUBLIC,ACC_PRIVATE、ACC_PROTECTED、ACC_STATIC、ACC_FINAL,ACC_VOLATILE以及ACC_TRANSIENT标记用来计算serialVersionUID的值,针对构造函数和方法的修饰符包括ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED、ACC_STATIC、ACC_FINAL、ACC_SYNCHRONIZED、ACC_NATIVE、ACC_ABSTRACT和ACC_STRICT标记。名称和描述符使用java.io.DataOutputStream中的writeUTF方法中的格式写入。
字节流中的项的顺序如下:
long hash = ((sha[0] >>> 24) & 0xFF) | ((sha[0] >>> 16) & 0xFF) << 8 | ((sha[0] >>> 8) & 0xFF) << 16 | ((sha[0] >>> 0) & 0xFF) << 24 | ((sha[1] >>> 24) & 0xFF) << 32 | ((sha[1] >>> 16) & 0xFF) << 40 | ((sha[1] >>> 8) & 0xFF) << 48 | ((sha[1] >>> 0) & 0xFF) << 56;
v.可序列化对象的版本
Java对象在序列化的过程中会把状态数据存储在文件中,或者在数据库中保存为blob格式,但是序列化写入时对象的版本【非上边提及的序列化版本05】和反序列化读取时的版本有所不同,类的版本会存在潜在的提升。
版本提升是一个基础问题,它包含了如何标识一个类,包括这个类中构成的兼容性改变【Compatible Change】,兼容性改变表示不影响类和其调用者合约的改变。
这个章节将会讲解目标、假设以及相关解决方案。这些内容有助于开发人员在小心选择机制而引起类似改变的过程中更容易定位问题的来源;这里提到的解决方案会在一个类通过添加字段和添加类进行逐渐演化的时候提供一个针对该类的引用Handle的”自动化“机制,序列化操作不需要每个版本都去实现类里面特定的方法,它还是可以得到版本号,同样字节流数据的格式也可以不通过调用类的特定方法来处理。
1)目标
2)假设【注意这里使用了类进化概念”evolve“,进化过的类可表示”新版本“类,未进化过的类可表示”旧版本“类】
3)谁负责字节流的版本?
在类的不断演化过程中,进化过的类【最新版本】的职责是维护它和未进化的类【旧版本类】之间设置的合约。这个有两种形式:
第一,进化过的类禁止打破和原始版本提供的接口相关的存在假设,这样进化过的类就可以取代原始的类;
第二,当进化过的类和原始版本【早期版本】进行通信时,它必须提供足够和等效的信息,使其能够继续满足和没有未进化的类【旧版本】之间的合约;
和上边讨论的目的一样,每一个类需要实现和继承于某个接口或者和父类之间定义的合约。新版的类,例如图中的foo',必须满足foo拥有的合约而且它有可能实现某个接口而修改其实现。通过序列化在对象之间进行通信并不属于这些接口定义的合约,序列化是各种实现之间的一个私有协议,它的责任是使得所有的实现能够有效地通信,同时允许每一个实现继续满足其客户端期望的合约【这里把和某个对象通信的另一端称为其客户端】。
4)兼容的Java类型演化
Java的语言规范讨论了Java中的类在进化过程其二进制码的兼容性,大部分二进制码的兼容性来自于Java中类、接口、字段、方法等符号引用的延迟绑定。
下边是设计Java中可序列化的对象流的原则:
5)类型变化影响序列化
基于上边的概念,我们现在可以描述如何设计针对类进化的不同情况,类的某些版本从字节流写入的角度描述了这些情况,当这些字节流被同样版本的类读回时,不会出现功能和信息的丢失,对原始的类而言只有字节流是信息源。它的类描述——原始类的类描述集,足以与重组类的版本字节流中的数据匹配。
不兼容的改变【无法维护互操作性的变更】:
兼容的改变:
vi.对象序列化流协议
对象字节流的格式将满足下边的设计目标:
1)字节流元素【或者称为项】
基本的结构需要描述字节流中的对象,对象的每一种属性都应该在字节流中体现:对象所属类、对象的成员属性【字段】,这些数据会被写入而且之后会被类中特定的方法读取,字节流中对象的描述会使用固定的语法。null对象、新对象【new objects】、类【classes】、数组【arrays】、字符串【strings】、任何在流中存在的对象的反向引用【back references】都会有特殊的描述信息,每一个写入字节流的对象都会被赋予引用Handle,这个引用Handle可以反向引用该对象。该引用Handle会从0x7E0000开始进行顺序赋值并且自动自增,一旦字节流发生了重置则该引用Handle会重新从0x7E0000开始。
一个类的实例化对象使用下边结构描述:
一个类的ObjectStreamClass对象描述非动态代理类按照下边规则:
一个类的ObjectStreamClass对象描述动态代理类时按照下边规则:
字符串对象的描述信息会在字段描述符【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枚举常量的描述信息包含下边内容:
新对象【New objects】的描述信息包含下边内容:
所有被类写入流的基础类型的数据都会被缓冲以及包装在【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] | 预定义标记的数量,也是一个数组匹配项的数目。 |
x0001 | 16进制数据格式的字面量,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中定义:
final static short STREAM_MAGIC = (short)0xaced; final static short STREAM_VERSION = 5; final static byte TC_NULL = (byte)0x70; final static byte TC_REFERENCE = (byte)0x71; final static byte TC_CLASSDESC = (byte)0x72; final static byte TC_OBJECT = (byte)0x73; final static byte TC_STRING = (byte)0x74; final static byte TC_ARRAY = (byte)0x75; final static byte TC_CLASS = (byte)0x76; final static byte TC_BLOCKDATA = (byte)0x77; final static byte TC_ENDBLOCKDATA = (byte)0x78; final static byte TC_RESET = (byte)0x79; final static byte TC_BLOCKDATALONG = (byte)0x7A; final static byte TC_EXCEPTION = (byte)0x7B; final static byte TC_LONGSTRING = (byte) 0x7C; final static byte TC_PROXYCLASSDESC = (byte) 0x7D; final static byte TC_ENUM = (byte) 0x7E; final static int baseWireHandle = 0x7E0000;
而classDescFlags的标记包含下边几种:
final static byte SC_WRITE_METHOD = 0x01; //if SC_SERIALIZABLE final static byte SC_BLOCK_DATA = 0x08; //if SC_EXTERNALIZABLE final static byte SC_SERIALIZABLE = 0x02; final static byte SC_EXTERNALIZABLE = 0x04; final static byte SC_ENUM = 0x10;
这里不介绍上边的类型标记了,仅仅介绍了五种新标记:
SC_WRITE_METHOD:
如果一个可序列化的类重写了writeObject方法,而且向字节流写入了一些额外的数据,则这个标记就会被设置;这种情况下,一般使用结束符TC_ENDBLOCKDATA来标记这个对象的数据结束;
SC_BLOCKDATA:
如果一个可外部化的类在写入字节流数据的时候使用了STREAM_PROTOCOL_2的协议,则这个标记会被设置;默认情况下在JDK 1.2中使用该协议写入外部化的数据,JDK 1.1中使用的流协议是STREAM_PROTOCOL_1;
SC_SERIALIZABLE:
如果写入的对象的类实现了java.io.Serializable接口,而没有实现java.io.Externalizable接口,这个标记就会被设置;从字节流中读取该对象相关数据的时候必须保证序列化的对象所属类实现java.io.Serializable接口,这样就可以直接使用默认序列化的机制来读取字节流中的数据;
SC_EXTERNALIZABLE:
写入的对象的类必须实现java.io.Externalizable接口,这种情况就会设置这个标记;从字节流中读取该对象相关的数据的时候必须保证序列化的对象所属类实现了java.io.Externalizable接口,则系统会调用writeExternal方法和readExternal方法来执行序列化和反序列化的操作;
SC_ENUM:
当写入的对象的类是一个Enum枚举类型的时候,这个标记就会被设置;枚举类型的写入和读取遵循规范中定义的枚举类型的读写规则;
到这里所有规范的内容都讲解完了,针对上边提到的语法这里提供一个更加详细的图,方便读者彻底理解Java序列化中字节流的语法:
上图中绿色部分是子节点,有些地方没有子节点是因为图中针对语法树已经有说明了,所以请读者阅读上图的时候细心!上边的图的清晰度不是特别高,有兴趣的读者可以自己去分析上边的语法自己绘制一颗语法树来体会Java序列化的目标数据的二进制格式,再结合前边章节提供的示例彻底理解Java中的内建序列化。