19.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.对象的状态

  先看一段代码:

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类型的对象,该对象有两个基本属性nameage。那么什么叫做对象的状态呢?——状态这个概念在网上一直有争议,这里归纳一下个人观点:理解对象的状态要从两个不同的角度进行,机器的角度和人的角度;从机器的角度来讲,对象的状态表示存储空间中对象的各个属性的瞬时值的一个集合,每修改一次对象的属性,对计算机本身而言其状态已经发生了改变;从人的角度来讲,对象的状态表示人在关注对象的时候对象在关注点的各个属性的瞬时值的一个集合,而我们在编程过程理解的对象状态一般只考虑关注点的对象。看看上边代码里面的对象的状态变化初始化状态为对象状态变化的起点
机器的角度来讲,上边的状态修改图如下:


  从上图可以知道,对机器本身而言,状态改变了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例子中,只要数据库中的nameage不发生任何变更,则对象不论恢复多少次的初始化状态就始终是:

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的对象转换成为字节序列的过程称为Java的序列化过程
  • 将字节序列转化成为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("silentbalanceyh@126.com");
		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                                                          
silentbalanceyh@126.com 

  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接口的子接口。对比一下这两个接口:

接口名称SerializableExternalizable
实现方法内建支持,使用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文件来分析内建序列化自定义序列化【外部化的差异,上述代码的输出为:
ExternalObject [name=[name=Lang Yu], age=27]

  java.io.ObjectInputStream java.io.ObjectOutputStream——在Java语言里,这两个类又称为对象流输入输出类,ObjectOutputStream负责将Java对象的基本数据、对象数据以及图形数据写入OutputStream,并且可使用ObjectInputStream进行读取,恢复重构该对象——实际上是读取该对象序列化时的状态,然后根据它的状态重新创建一个属性数据和原始对象一样的新对象,可以看做原始对象的副本,从数据使用上讲其用法和原始对象的使用是一致的,所以又可以称为“恢复&重构原始对象”;通过在流中使用目标介质文件类似前边的serial.objextserial.obj可以实现对象的持久存储,若是网络套接字,则可实现在另一台主机以及另一个进程中恢复重构该对象。
  这两个类对其操作目标对象有几个注意点需要说明仅针对默认的内建API实现

  • 操作对象必须是实现了Serializable接口,即对象必须包含“可序列化的语义”
  • 对象的属性在序列化的过程会被递归序列化,所以当某些对象的属性本身是自定义Class类型的时候,该类型也必须包含“可序列化的语义”
  • 网络中传输对象的时候,被传输的对象也必须具有“可序列化的语义”
  • 在对象中静态属性不会被序列化,transient关键字修饰的内容也不会被序列化

  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("silentbalanceyh@126.com");
		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();
	}
}
  先看看这段代码的输出:
true
TransientKeyword [name=null, age=27]
120
silentbalanceyh@126.com

  对比前边的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修饰的对象的特定数据域在执行序列化和反序列化的操作不会被序列化,它具有“不会序列化的语义;
  • 当Java对象被反序列化恢复状态的时候,程序并不会调用该对象的构造函数,它会直接读取该对象在序列化时的状态,并使用该状态中的属性值填充当前对象;
  • transient关键字只能修饰Java对象的成员属性不可修饰Java对象的成员方法也不可修饰

  transient的内容先讲到这里,等到读者理解了序列化的原理过后再回头来看看几个比较争议的问题。

  3)特殊常量serialVersionUID

  serialVersionUID常量的作用:Java在执行序列化的时候为了保持版本的兼容性,即JDK版本升级的时候在反序列化的过程仍然保证Java对象的唯一性。这个值一般有两种生成方式:
  第一种:默认使用1L,比如使用下边这种定义:

private static final long serialVersionUID = 1L;

  第二种:根据类名、接口名、成员方法以及属性来生成一个64位哈希字段,其定义格式类似:

private static final long serialVersionUID = 3495065278062841972L;

  一般在类似EclipseNetbeans的开发工具中,都可以自动生成这个值。
  一个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("silentbalanceyh@126.com");
		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
silentbalanceyh@126.com

  但是这里我们不需要去关心这段代码的运行输出,我们集中精力在生成的文件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首先初始化序列化数据的格式。
  AC EDSTREAM_MAGIC:输出流“魔数”魔数这个概念相信很多程序员都不陌生,很多特定的类型文件中,起始的几个字节内容是固定的(要么是故意填充,要么本身就如此),程序根据这几个字节的内容就可以确定该文件的类型,因此这几个字节的内容被称为魔数【Magic Number。从这里可以知道:Java对象通过序列化到文件的时候,生成的二进制文件并不是一个普通的二进制文件,它具有特殊性,标识了该文件是通过Java序列化生成的,这里这个AC ED表示的就是这样的一个语义。
  00 05STREAM_VERSION:这个值标识了Java序列化字节流的版本号,这里的版本号可参考其定义接口java.io.ObjectStreamConstants
    /**
     * 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字节序列
  77TC_BLOCKDATA:表示可选的数据块,通常跟在此标记之后的数字表示该数据块字节数
  01(第二个)表示当前写入的数据(boolean类型的值true)使用了1个字节的存储空间

  01(最后一个)标识了boolean类型的数据值,若为true则该序列是01,若为false则该序列为00——也就是说如果上边writeBoolean的参数为false则该序列就变成了77 01 00

  ------------------writeObject------------------------

  ==>SerialInner元数据信息

  从下边的序列开始,就是执行下边这句代码生成的内容:

out.writeObject(person);

  73 72 00 21:该序列写入的是对象的基本信息
  73TC_OBJECT:该标记是一个声明标记,表示需要序列化的对象是一个新对象
  72TC_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个字节的二进制序列
  02 00 01:该序列包含当前这个对象是否支持序列化的信息,以及该类定义中成员属性数量信息
  02这个标记表示该对象是支持序列化的;

  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序列化中具有的作用。
  49 00 03 61 67 65:这段二进制序列描述了当前对象中的属性的信息,因为只有一个属性age,描述的信息自然是age属性的信息实际上是age的元数据信息
  49此值转化为字符对应字符‘I’(本文将在后边介绍成员属性的类型代码),它表示了属性age的数据类型为int
  00 03该值表示了成员属性名称长度——"age".length() == 3

  61 67 65该值表示了成员属性名称本身61转化成Unicode的十进制值为97,即小写字母"a"67表示"g"65表示"e",三个数字连接在一起表示age,即属性age的定义名称。)

  ==>SerialBase元数据信息【父类】

  78 72 00 20:该序列表示org.susan.java.serial.SerialInner类实例化的对象描述信息的结束,以及它的父类对象描述信息的开始
  78TC_ENDBLOCKDATA:这个标记是数据块结束的标记,它表示org.susan.java.serial.SerialInner类实例化的类描述信息结束,一般二进制序列中所有的对象的类描述信息结束都会使用该标记,以表示对象描述的终止符
  72TC_CLASSDESC:该标记表示一个新类的描述信息的开始,在示例中它表示父类org.susan.java.serial.SerialBase类描述信息开始
  00 20该标记和前面描述子类的信息一致,它表示父类类名长度0x20的值转化成十进制表示32,即——org.susan.java.serial.SerialBase(32字符长度)
  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
  上述32个字节的数据表示的就是当前对象所属类的父类类全名,即字符串:org.susan.java.serial.SerialBase
  30 80 F7 5A 4D BC D0 74
  这里相信不用解释读者也明白,它表示父类中定义的serialVersionUID的值,代码中的定义如下:

	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:这段序列表示了从子类到父类的引用信息,有多少个引用就出现多少个类似序列,包括引用类型名长度以及类型名;
  74TC_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:这两个二进制序列表示父类的类描述信息结束。
  78TC_ENDBLOCKDATA:这个标记是对象块结束的标记,表示org.susan.java.serial.SerialBase的描述信息结束了;
  70TC_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.SerialBaseage属性的,换算成十进制数据值为26,对应下边的定义代码:

private int age = 26;

  70TC_NULL:这个标记代表一个空引用(这里的TC_NULL只是一个空引用的数据,并不是递归继承终止符),它表示父类org.susan.java.serial.SerialBasename属性的name未进行任何初始化,根据Java的语言规则,它的值为null,对应下边的定义代码:

private String name;

  00 00 00 1B这段数据表示子类org.susan.java.serial.SerialInnerage属性的值,虽然该属性未进行初始化,但是在代码执行序列化之前,该属性拥有值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的详细信息。
  77TC_BLOCKDATA:可选的数据块,参考后边的章节就知道,所有基础类型数据的序列化都会使用数据块的结构;
  04该值表示当前写入数据120值占用的字节数,因为是int类型的数据,所以这个120的数据应该占用4个字节;
  00 00 00 78这段数据表示120十六进制0x78转换成十进制就是120

  ------------------writeObject------------------------

  示例代码的最后一部分写入了一个常量字符串,所以下边这部分序列对应的代码为:

out.writeObject("silentbalanceyh@126.com");

  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:这段信息描述了常量字符串的定义信息。
  74TC_STRING:这个标记表示一个new String(新的字符串对象)
  00 17这个值换成十进制25,它表示了字符串"silentbalanceyh@126.com"长度
  73 69 6C 65 6E 74 62 61 6C 61 6E 63 65 79 68 40 31 32 36 2E 63 6F 6D这一段序列表示的就是字符串"silentbalanceyh@126.com"

  到这里,上边的示例就解析完了,通过这个例子是否理解Java内建序列化的基本用法了呢?简单总结下:

  • 针对基础数据,一般使用数据块的方式(Data Block)存储数据,类似上边出现过的true120
  • 一旦使用了transient修饰的成员属性具有”不会序列化“的语义,序列化的时候会忽略
  • 除开serialVersionUID以外,所有的static修饰的成员属性隶属于类对象,所以它在序列化的时候同样会被忽略
  • 序列化数据的时候和访问控制修饰符无关,也就是说只要是成员属性不在乎它是private、protected还是public,序列化机制只关心对象的成员属性是否有transient关键字修饰;
  • 在序列化某个对象类描述信息【元数据部分的时候,序列化当前对象所属类的描述信息,然后从下至上递归序列化它的父类类描述信息;而在序列化实例数据信息【数据部分的时候,序列化父类中的成员属性,然后从上至下递归序列化成员属性,两部分数据生成的顺序刚好相反;
  • 只有成员属性才会被序列化,成员函数不会被序列化,也就是说序列化只针对对象的属性,而不针对对象的操作

  而Java内建序列化算法在序列化一个定义的Java对象的时候其顺序如下:

  • 先序列化某个对象的类描述信息【元数据部分,信息包括:类型,类名的长度,类名的值
  • 然后处理该对象内的成员属性描述信息,若成员是对象则递归序列化该成员属性;
  • 该对象信息处理完成过后,递归序列化该对象所属类的父类类描述信息(包括父类中的成员信息,同上边两步),直到没有超类为止(这里没有超类表示该类属于Object的直接子类)
  • 类描述信息【元数据部分序列化完成过后,序列化实例中的实例数据信息【数据部分,从顶级父类开始;
  • 从上至下递归序列化实例数据,直到当前对象为止;


  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

  这里不再累赘分析上边的二进制文件,详细看看这段代码生成的二进制数据。
  77TC_BLOCKDATA:可选的数据块,在Java序列化生成的二进制文件中,一段连续存储在一起的基础类型数据使用这个标记开始,前一个章节的例子有两处使用了TC_BLOCKDATA,其对应代码为:

		out.writeBoolean(true);
		out.writeObject(person);
		out.writeInt(120);

  因为在true120两个基础数据之间多了一个对象的序列化,所以序列化中每次遇到独立的基础数据段就会使用77TC_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表示true00表示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 20Short值32,占用了2字节空间
  00 00 00 20Int值32,占用了4字节空间
  00 00 00 00 00 00 00 20Long值32,占用了8字节空间

  3)Char、Byte数据的写入
  示例中的代码段为:

		// 针对Char的序列化
		out.writeChar('a');
		// 针对Byte的序列化
		out.writeByte(18);

  00 61 12:这一段二进制序列表示了Char、Byte类型的数据。
  00 61Char值,转换成十进制为97,字符'a',Java中的Char值是Unicode,占用2字节空间
  12Byte值,转换成十进制为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 CDFloat值,表示3.2f,占用4字节空间
  40 09 99 99 99 99 99 9ADouble值,表示3.2,占用8字节空间
  有必要在这里简单讲讲Flout和Double的格式问题:浮点数的存储格式和整数不一样,一个float格式主要分成了3部分Java中使用的浮点规范为IEEE765
符号位1(31):最高位,表示float数据的正负,01
幂指数8(23-30):表示2进制的幂次;
有效位24(0-22加上省略位):低23位,表示有效数字——二进制数的规格化表示中,小数点前的数为1(即二进制表示的最高位),所以一般省略,这样可以理解下边步骤中第4步为什么在高位追加1。
  上边的Float数40 4C CC CD展开成2进制为:
01000000 01001100 11001100 11001101
  将上边的格式转换:
0 10000000 10011001100110011001101
  转换步骤如下:

  1. 最高位是0表示该浮点数是正数
  2. 接下来的8位10000000是指数位,转换成十进制128减掉127其结果为1
  3. 尾数的23右移1位为1.001100 11001100 11001101
  4. 最前面添加1变成11.001100 11001100 11001101
  5. 整数部分值为11,转换成十进制是3
  6. 小数部分001100 11001100 11001101,转换成十进制0.1999863147735595703125 0.2

  结果相加约为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

  为什么会出现这种情况?其实原理很简单,Int80000被序列化过后其生成的二进制序列为:【00 01 38 80】,因为基础数据在存储的时候使用的存储格式是连续的字节序列,从这个例子可以看出:基础数据在序列化成字节过后本身没有“类型”的概念,那么类型是怎么产生的呢?——类型是在反序列化的时候代码中的方法调用时产生的。上面的例子序列化了一个Int数据到目标介质,而在反序列化的时候却调用了readShort的方法,从字节长度上看程序不会有错,但是00 01 38 80被当成了两个Short值处理,00 01转换成Short值是138 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中的ShortChar都是属于数字类型,但writeShortwriteChar均可以接受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 ……

  上边仅仅截取了前半部分的内容,先简单总结下这段代码

  • 基础数据的包装对象有两种方式实现序列化操作:数据方式对象方式
  • 数据方式仅仅针对JDK 1.5以及1.5以上的版本有效,上边代码在JDK1.4平台会报错,1.4只能使用writeObject方法进行序列化;
  • JDK 1.5过后,基础数据和包装对象若调用write前缀的方法生成的二进制序列是一模一样的;

  对比前一个章节的例子会发现,从第一行的第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对象的类描述信息
  73TC_OBJECT:该标记是一个声明,表示序列化的是一个新对象
  72TC_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对象序列化结束部分。
  78TC_ENDBLOCKDATA:该标记表示Boolean.TRUE对象的类描述信息【元数据部分结束;
  70TC_NULL:在递归序列化类描述信息【元数据部分的时候,发现Boolean类没有超类,它的直接父类是Object,所以输出此标记;
  01该标记表示值true,上边已经说过了Boolean类型的序列化字节结构01表示true00表示false
  到这里第一行代码writeBoolean(Boolean.TRUE就结束了,接下来看看剩余部分的二进制序列:
  73 71 00 7E 00 00 00:这一段二进制序列描述了下边这行代码执行过后的数据:

		out.writeObject(Boolean.FALSE);

  73TC_OBJECT:该标记是一个声明,表示序列化的将是一个新对象
  71TC_REFERENCE:该对象的类型,这里创建的类型为一个Boolean类的引用
  00 7E 00 00baseWireHandle:这个值是一个常量,它表示第一个赋值句柄,它的定义代码如下:

    /**
     * First wire handle to be assigned.
     */
    final static int baseWireHandle = 0x7e0000;

  00该标记表示值false
  到这里这个Boolean对象序列化过后生成的二进制序列就解析完成了,从上述的解析可以知道,基本数据包装类型使用对象方式序列化的时候,其序列化的规则和一个对象序列化的规则是一致的。


  v.对象引用探索

  前一个章节的末尾使用了常量TC_REFERENCEbaseWireHandle这两个值究竟使用了什么方式来实现序列化数据中这么多对象的管理呢?先看看例子:

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 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 72TC_OBJECT TC_CLASSDESC开始,78 70TC_ENDBLOCKDATA TC_NULL结束,随后跟上其对象中的属性值列表。Integer类中只有value属性,first这个Java引用对应的Integer对象其value属性值为2;Boolean类中也只有value属性,third这个Java引用对应的Boolean对象其value属性值为true,类似上使用了省略号被省略部分的二进制序列
  • 关于Java对象的创建——Java在序列化的过程中,创建对象的顺序如下:
    1.若创建1个新的Java对象,输出73TC_OBJECT表示当前创建的是一个新的对象;若不创建新的Java对象,则输出71TC_REFERENCE标记;
    2.判断当前环境中是否有创建对象的类描述信息【元数据部分,如果没有类描述信息使用72TC_CLASSDESC标记,如果已经序列化过类描述信息则使用71TC_REFERENCE标记(已经输出过该标记不输出第二次)
    3.类描述信息输出完成后(78 70结束),直接按照类中定义的属性顺序输出对象中属性的值列表;
    4.如果是使用的71TC_REFERENCE标记,需要分为两种情况:创建Java新对象 or直接写入引用,其格式如下(接着【73 71】之后或者【71】之后)
    ——创建Java新对象:先输出00 7E 00 00baseWireHandle变量,每次创建一个新对象的时候都会输出该变量,随后跟上对象的属性值列表;
    ——直接写入对象的引用:输出00 7E 00 XX格式(至少可以支持创建65536个新引用),这种情况不需要追加对象的属性值列表;
  • TC_REFERENCE的管理:71TC_REFERENCE标记之后,是一个整数Int类型的数据,它生成的基数是00 7E 00 00baseWireHandle常量,它的值表示了序列化中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,只列出了数据常量):

变量名称十六进制值十进制值含义
baseWireHandle00 7E 00 008257536
该值一般位于TC_REFERENCE之后,为计数器的基数,它一般表示第一个赋值的句柄;
STREAM_MAGICAC ED-21267Java序列化数据中输出到目标文件“魔数”
STREAM_VERSION00 055序列化协议中的版本信息,一般位于STREAM_MAGIC之后
TC_ARRAY75117标记接下来序列化的内容是一个数组对象
TC_BLOCKDATA77119标记接下来的一段数据是一个可选数据块的内容,跟随其后的int类型数字表示了之后的数据字节数
TC_BLOCKDATALONG7A122TC_BLOCKDATA,只是跟随其后的是一个long类型数字,它同样表示了数据字节数
TC_CLASS76118该标记用于引用一个类,实际上此标记就是一个Class的引用标记
TC_CLASSDESC72114该标记一般位于TC_OBJECT,用于描述当前序列化对象的类描述信息【元数据部分
TC_ENDBLOCKDATA78120该标记用于表示一个Java对象的描述结束,一般为对象描述终止
TC_ENUM7E126该标记在JDK 1.5过后有效,表示接下来的数据是一个枚举常量值
TC_EXCEPTION7B123该标记表示接下来的数据是一个异常对象,一般是一个Exception的对象
TC_LONGSTRING7C124该标记表示接下来的数据是一个长字符串对象,一般是长度超过了某一个固定的值
TC_MAX7E126该标记表示最后一个标记值
TC_NULL70112此标记表示null,用于描述对象的空引用
TC_OBJECT73115该标记是一个新对象的声明,表示接下来的数据是新创建的一个对象
TC_PROXYCLASSDESC7D125该标记一般位于TC_OBJECT之后,表示当前Java对象是一个代理类对象
TC_REFERENCE71113该标记表示引用,其表示接下来的数据类型是Java引用类型
TC_RESET79121重置标记,意味着对象流中的数据会被重置
TC_STRING74116该标记表示当前序列化对象是一个new String字符串对象
  对于对象中的属性,它的类型标记和上述表格中的标记不一致,上边提到过:4C是字符'L',它表示当前属性是一个String,接下来看看类型的编码对应表只针对对象中的成员属性的类型

十六进制值对应的字符字段的类型
42Bbyte
43Cchar
44Ddouble
46Ffloat
49Iint
4AJlong
4CL类或者接口类型
53Sshort
5AZboolean
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进行比较)。简单总结基本数据作为成员属性的序列化规则:

  • 成员属性二进制序列中,定义顺序【元数据使用顺序【数据部分的位置是一致的,如上定义部分的顺序为int、long、char、short、boolean、byte、double、float、Integer类,使用这些成员属性的时候数据顺序和定义这些成员的数据顺序一致
  • 不论数据的值是多少,其使用的时候高位都用0填充保证其二进制序列的类型所占用的字节数,比如int的数据27,数据本身只占用1字节,但是因为是int类型,所以序列中这个数据是4字节00 00 00 1B
  • 如果成员属性是一个对象,则在元数据定义描述的序列之后,会创建一个TC_STRING的引用来引用该成员属性,这段二进制序列的描述会直接在该成员属性的元数据之后;
  这个章节通过几个比较详细的例子讲解了Java序列化生成的二进制序列的结构,从数据结构的角度剖析了Java内建序列化的算法以及其序列化原理,从下一个章节开始,本文将从另外一个角度来看看Java内建序列化的相关算法和原理。实际上在上边的分析中还有很多待解决的疑点,不过读者放心,本文会尽可能提供更加详细的解释让大家深刻去理解Java序列化,读者可以自己尝试用上边讲到的方法来分析序列化生成的二进制文件,以加深对Java内建序列化的理解。


3.深入序列化规范

  本章节大部分内容来源于对JVM的对象序列化规范的解读,有兴趣的读者可以直接查看该规范
  为了不误导读者,下边的”成员属性“”字段“表示同一术语,只要读者可理解即可;而下边提到的字节流的英文并不是对应byte stream,而是stream,本来在写的时候准备直接使用”流“作为术语,但是考虑到理解的时候字节流对Java开发人员更加容易懂得,而且当使用ObjectOutputStream作为输出的时候其数据本身就是字节数据,所以采用了”字节流“为术语,其表示内容对应英文中的stream。因为我的翻译有限,只是在阅读基础之上加入了相关的描述,所以如果读者有无法理解的部分还是参考原规范为最佳,我只是为了写本文而参考,不保证翻译的精确性,但我会尽可能把语言整理得让读者容易理解。
  http://docs.oracle.com/javase/7/docs/platform/serialization/spec/serialTOC.html


  i.系统结构

  1)序列化目标
  JDK中设计序列化机制的目标如下:

  • 为Java中的对象数据的处理提供一个简单的可扩展机制;
  • 使用序列化的方式来维护Java对象的对象类型以及其相关属性;
  • 针对在网络中需要传输的远程对象Remote Object,提供可扩展的方式来支持marshalingunmarshaling的机制;
  • 使用可扩展的方式支持Java对象的简单持久化Persistence
  • 针对每一个类的实现提供Java对象的自定义;
  • 允许让开发人员自定义外部的数据格式来存储Java对象;

  2)写入对象流
  Java语言中将一个Java对象转换成数据流的格式的过程代码如下:

    // 将一个时间对象序列化成对象数据流
    FileOutputStream f = new FileOutputStream("tmp");
    ObjectOutput s = new ObjectOutputStream(f);
    s.writeObject("Today");
    s.writeObject(new Date());
    s.flush();

  在Java语言中,序列化数据的基本规则如下:

  • 针对Java基础数据的序列化使用接口java.io.DataOutput,该接口包含类似writeInt、writeBoolean等方法;
  • 针对对象数据的序列化使用接口java.io.ObjectOutput,该接口包含writeObject方法;实际上ObjectOutput接口是DataOutput子接口,它们之间存在继承关系
  • 针对数组的序列化使用类java.io.OutputStream,该类主要用于流数据的写入处理,可以序列化字节数组处理成字节流数据

  看看下边的图来理解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语言中,反序列化数据的基本规则如下:

  • 针对Java基础数据的反序列化接口java.io.DataInput,该接口包含了类似readInt、readBoolean等方法;
  • 针对对象数据的反序列化使用接口java.io.ObjectInput,该接口包含readObject方法;实际上ObjectInput接口是DataInput子接口,它们之间存在继承关系
  • 针对数组的反序列化使用类java.io.InputStream,该类主要用于流数据的读取处理,可反序列化通过字节流重建Java对象;

  看看下边的图来理解Input部分的接口和类的整体结构:


  4)对象流容器
  Java中的对象序列化机制生产和消费的都是字节流数据【上边示例中的二进制序列,这些字节流里面可能包含一个或多个Java基础类型数据以及Java对象数据——如果Java对象写入到流数据中引用了其他Java对象,这个字节流中同样也会描述这种关系。实际上Java对象充当了一个流数据容器,它提供了读取和写入字节流数据的接口,这两个接口就是ObjectOutputObjectInput

  • 这两个接口提供了写入和读取字节流
  • 将Java基础类型数据或者Java对象数据写入字节流
  • 从字节流中读取存储的Java基础类型数据或者Java对象数据

  如果一个Java对象要充当序列化中的流容器,它必须显示声明自己符合了JVM的序列化协议【通过实现java.io.Serializable接口,这样的Java对象才能将自己的状态写入字节流【序列化以及从字节流中读取Java对象状态重建该Java对象反序列化。JVM中定义了两套协议用于这种操作:

  1. 实现Serializable接口
  2. 实现Externalizable接口

  5)类中定义”可序列化“字段
  在一个类中定义”可序列化“的字段有两种不同的办法;默认情况下——一个类里面只要字段的定义是transient或者非静态的定义不使用transientstatic关键字,那么这种字段就是可序列化的,使用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

  • @serial标记用在Java多行注释中,用于注释一个可序列化的成员属性,其语法为:@serial 字段描述信息“,描述信息中一般包括该成员属性的含义和可接收的值的范围,而且这个描述信息可放在注释中的多行中;
  • @serialField标记主要用于描述成员属性serialPersistentFields数组中的每一个ObjectStreamField类型的元素,这个标记在使用的时候最好针对每一个元素进行注释,其语法为:@serialField字段名称字段属性 该成员字段【成员属性】 的详细描述信息“
  • @serialData标记用于描述序列化过程中每一个成员的读写顺序,其语法为:@serialData 该数据的详细描述信息“

  7)操作类定义中的可序列化字段
  Java序列化机制提供了在字节流中操作序列化字段的两种方法:

  • 默认机制下,不需要任何字段的定制化操作
  • Java序列化机制中的字段API提供了显示定制方式,包括定制字段的mapping信息

  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中安装了安全管理器,当构造函数被子类的构造函数直接或者间调用时,子类若重写了putFieldswriteUnshared方法,这个构造函数还会检查”enableSubclassImplementation“ SerializablePermission以确定代码的执行权限
  这个类中最核心的方法是writeObject方法,其函数签名如下:

  1)writeObject

public final void writeObject(Object obj) throws IOException

  前边的示例中在分析序列化生成的二进制序列的时候多次提到这个方法,这个方法在序列化一个Java对象的时候会遵循下边的规则后边源码分析会详细说明

  1. 如果子类重写父类的某些实现,则调用writeObjectOverride方法;
  2. 如果在Block-Data类型缓冲区中存在数据,则先将这些数据从缓冲区写入到字节流,然后重置缓冲区;
  3. 如果一个对象是nullnull值就直接被写入到字节流
  4. 如果一个对象之前已经被替换过什么叫"替换"?参考第8步的描述,将这个替换对象的引用Handle写入到字节流;
  5. 如果一个对象已经被写入了字节流,它的引用Handle直接写入到字节流;
  6. 如果一个对象是一个Class类型,则一个ObjectStreamClass对象将会写入字节流,对应的引用Handle会赋值给该Class;
  7. 如果一个对象是一个ObjectStreamClass对象,先将一个引用Handle赋值给这个对象,然后直接将这个对象的类描述信息【元数据写入到字节流。在JDK1.3以及之后的版本中,如果ObjectStreamClass类型的对象描述的是一个动态代理类,则writeClassDescriptor方法调用的时候会直接输出该对象的类描述信息,检测一个对象的类是否动态代理类可使用java.lang.reflect.ProxyisProxyClass方法,之后会在字节流中写入一个标记来描述该类。如果该对象的类型是一个动态代理类,则它会调用annotateProxyClass方法来提取类描述信息,相反则调用annotateClass方法来提取类描述信息。
  8. 处理Java对象所属类的潜在替代类,或者被ObjectInputStream的子类处理的潜在替代类
    a.如果一个对象所属的类不是Enum类型,而且它定义了期望的writeReplace成员方法,这个方法将会被调用。然后,它将返回一个已经被序列化过替代对象
    b.其次, 若调用了enableReplaceObject方法就启用了”对象替换“,则replaceObject方法也会被调用,它允许ObjectOutputStream对象的子类对当前对象的替代类执行序列化。如果原始对象在前边的步骤已经被替换过了,则替代对象会调用replaceObject方法;
    如果原始对象被上边的一步或者两步替换掉了,则从原始对象到替换对象的mapping也会被记录下来,然后在这个新对象上重复第3步到第7步,而这个mapping会在执行过程中的第4步中使用;如果替代对象不属于第3步到第7步包含的类型,则到第10步中会唤醒替代对象
  9. 如果该对象的类型是java.lang.String,这个String对象会以UTF-8的格式写入字节流,先写入该String的长度、然后写入String的内容,最后将一个引用Handle指向该String
  10. 如果该对象是一个数组对象,则writeObject方法会递归调用写入数组的ObjectStreamClass信息,其次赋值引用Handle给这个数组,接着写入数组的长度,其次写入每一个元素的值;
  11. 如果对象是一个Enum常量类型,则writeObject方法会递归调用写入EnumObjectStreamClass信息,该常量只会在第一次被引用的时候出现,然后将引用Handle赋值给这个Enum常量。然后调用枚举类型中的name()方法以字符串的方式写入字节流,若这个字符串在前面的步骤已经出现过则使用引用的方式写入;
  12. 对于一般的Java对象,先使用ObjectStreamClass提取该对象的类描述信息,然后递归调用writeObject方法,但是这个信息只会在对象第一次被引用的时候出现,然后将引用Handle赋值给这个对象;
  13. 对象的内容写入规则:
    a.如果对象是可序列化的,先找到顶级父类,从这个类到每一个子类同一继承路径上依次写入类中的成员属性。如果这个类没有writeObject方法,就调用defaultWriteObject方法将成员属性写入;若这个类包含了writeObject方法,就调用writeObject方法,这个方法可自定义,它有可能调用defaultWriteObject方法或者调用putFieldswriteFields方法来保存对象的状态,然后将其他数据写入字节流;
    b.如果一个对象使用了外部序列化实现Externalizable接口,则调用writeExternal方法;
    c.如果一个对象不是可序列化的而且没使用外部序列化,则抛出NotSerializableException异常

  2)writeUnshared

public void writeUnshared(Object obj) throws IOException

  这个类中有一个writeUnshared方法,这个方法会把”非共享“的对象写入到字节流,而且每次写入对象时都把对象当做一个新对象处理;

  • 如果使用writeUnshared方法序列化对象,不管这个对象之前是否已经写入到字节流中了,系统每次都会把这个对象当做新对象处理;
  • 如果调用writeObject方法的时候发现这个对象之前已经被writeUnshared写入到字节流了,writeObject方法还是会把这个对象视为独立对象,不为之前写入的对象生成引用,也就是说ObjectOutputStream类不会为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方法在遇到特别的类ClassObjectStreamClass)时不会被调用。子类的实现将会返回一个替代对象,它将替代原始对象执行序列化操作,这个替代对象必须是可序列化的,而所有字节流中的指向原始对象的引用也会被替换使其指向替代对象。
  当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写入的魔数信息以及序列化版本是否匹配。若已经安装了安全管理器,如果子类重写了readFieldsreadUnshared方法,则这个构造函数同样会调用安全管理器以确认执行代码是否包含了“enableSubclassImplementation” SerializablePermission的权限。

  1)readObject

    public final Object readObject() throws IOException, ClassNotFoundException

  ObjectOutputStream类相对应的,ObjectInputStream类具有一个核心的反序列化的方法readObject,其方法会重构字节流中的Java对象,其规则如下:

  1. 如果ObjectInputStream类的子类重写父类的实现,系统将会调用readObjectOverride方法;
  2. 如果在字节流中发现了Block-Data类型格式的数据,则针对合法字节的数量抛出一个BlockDataException的异常;
  3. 如果字节流中的对象为null,则返回null
  4. 如果字节流中的对象是一个指向先前对象的句柄,则返回它指向的对象;
  5. 如果字节流中的对象是一个Class类型,则读取它的类描述信息【元数据,对应的ObjectStreamClass,将它和它对应的引用Handle添加到“已知对象【known objects】”的集合,然后返回Class类型的对象;
  6. 如果字节流中的对象直接是ObjectStreamClass类型,则从它的格式中读取数据,同样添加它和它到引用Handle“已知对象”的集合。在JDK1.3以及之后的版本中,如果一个ObjectStreamClass的类描述的元数据信息不是Java中的动态代理类字节流中会有说明,则readClassDescriptor方法将会被调用。如果一个类描述信息描述的是一个Java中的动态代理类,则系统会调用resolveProxyClass方法获取本地类的类描述信息,否则调用resolveClass方法来获取本地类的描述信息。如果一个类无法被识别处理,则抛出ClassNotFoundException的异常;
  7. 如果字节流中的一个对象是一个String,则先读取之后的长度信息,然后以UTF-8的方式读取字符串的内容信息,并且将这个重建的String对象以及相关引用添加到“已知对象”的集合中去;接着执行第12步的操作;
  8. 如果字节流中的对象是一个数组【array,读取这个数组的ObjectStreamClass信息以及它的长度值。然后分配该数组的存储空间并且将数组对象以及它的引用Handle添加到“已知对象”集合中,然后遍历这个数组的元素根据元素的类型恢复数组对象中的每一个元素,并把这些恢复的元素填充到当前数组对象中;
  9. 如果字节流中的对象是一个Enum枚举常量,就读取该枚举类型的ObjectStreamClass对象该枚举类型的元数据信息和该枚举常量的常量名称;如果读取的ObjectStreamClass信息不是描述的一个Enum类型,则抛出InvalidClassException异常。获取Enum枚举常量的引用的方法可以调用java.lang.Enum.valueOf方法,将ObjectStreamClass描述的枚举常量name信息当做参数传入该方法。如果valueOf方法抛出了IllegalArgumentException异常,则另外一个InviladObjectException异常将会抛出,其堆栈信息中会把IllegalArgumentException作为该异常的原因Root Cause。然后将该枚举常量以及其相关引用Handle添加到“已知对象”的集合中;接着执行第12步的操作;
  10. 针对所有标准的Java对象,直接从字节流中读取对象的ObjectStreamClass信息,然后从本地类中获取ObjectStreamClass信息。这个Java对象所属于的类定义必须是可序列化的或者是支持外部序列化【实现Externalizable接口】,而且这个类不能是Enum枚举常量类型,如果这个类不满足该条件,则抛出InviladObjectException异常;
  11. 其次为这个类的实例对象分配空间,将实例化过的对象和它的引用添加到“已知对象”集合中,它的内容处理过程如下:
    a.针对可序列化的对象,它会先调用它的所有父类中第一个不可序列化的父类无参构造函数。而针对可序列化的类,它所有的字段初始值会根据字段的类型赋默认值。每个类的字段在恢复的时候都会调用类中定义的特定的readObject方法,如果可序列化的类中没有定义readObject方法,则调用defaultReadObject方法。需要注意的是:在反序列化的过程中对应的类的字段初始化过程和构造函数都不会被执行。通常情况下,写入字节流的序列化版本号应该和从字节流中读取的序列化的版本号应该相同,这种情况下字节流中所有对象的超类才能够和ClassLoader载入的超类匹配。如果从字节流中读取的类版本信息和ClassLoader中载入类的版本信息不同时,则ObjectInputStream恢复对象状态和初始化对象的时候必须小心处理,这个类必须去追踪检测对应的类,匹配需要恢复对象的类信息和字节流中合法的数据信息。如果这些类信息出现在字节流中,但是它并没有出现在ClassLoader载入的对象中,这时反序列化过程放弃对象状态的恢复;如果这些类信息出现在ClassLoader载入的对象中,而字节流中没有出现该描述信息,则使用载入对象默认序列化的字段默认值初始化对象状态。
    b.针对可外部化的对象而言,当前对象的默认无参构造函数会被调用,其次调用readExternal方法来恢复定义对象的状态;
  12. 处理对象所属类的潜在替换,或者被ObjectInputStream的子类处理的潜在替换:
    a.如果对象所属的类不是枚举类型,而且定义了期望的readResolve方法,则这个方法会被调用并且允许当前对象自身被替换;
    b.如果之前调用过enableResolveObject方法,则调用resolveObject方法,它允许字节流中的子类检测和替换当前对象。如果前边的对象并没有替换原始对象,这个resolveObject方法将在替换过的对象中调用;
    如果替换之前发生过,“已知对象”集合表known object将会被更新,这样就会使得替换过的对象和引用Handle相关联,这种情况下readObject将返回被替换过的对象;

  在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方法被调用来反序列化一个反向引用(back-reference表示该引用引用的对象之前已经写入到字节流中了,一个ObjectStreamException异常将会抛出;
  • 如果readUnshared方法执行成功,任何一个想要依赖readUnshared方法反序列化一个反向引用的情况都会引起ObjectStreamException异常抛出;

  通过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方法的调用不能针对特殊的一些类型ClassObjectStreamClassString以及数组。子类若实现了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的子类应该调用ObjectInputStreamprotected修饰的无参构造函数,在这个无参构造函数会执行针对SerializablePermission "enableSubclassImplementation"安全检查以保证只有受信任的类可以重写默认的实现。这个构造函数不会为任何ObjectInputStream私有数据分配空间,它只是设置了一个标记告诉readObject方法去调用readObjectOverride方法。除开这个finalreadObject方法之外,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()

  一旦调用ObjectInputStreamresolveClass方法在本地虚拟机中找到了一个Class则返回该Class,否则返回null

  getFields

public ObjectStreamField[] getFields()

  该方法返回一个ObjectStreamField数组,用来描述一个类中所有可序列化的字段的集合。

  getSerialVersionUID

public long getSerialVersionUID()

  该方法返回一个类中定义的serialVersionUID属性,如果系统中的可序列化的类未定义该属性,这个值将会根据定义的类的类名、接口、方法、字段使用算法Secure Hash AlgorithmSHA计算一个新的。

  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方法的实参传入的是一个动态代理类则会有下边的结果:

  • 调用getSerialVersionUID()方法返回0L
  • 调用getFields方法将返回一个长度为0数组
  • 调用getField方法的时候,传入任何String的参数都会返回null

  3)序列化形成

  一个ObjectStreamClass类的实例的序列化形成过程依赖于传入的类是否为一个可序列化类【实现Serializable接口外部化类【实现Externalizable接口或者Java的动态代理类
  如果字节流中一个ObjectStreamClass的实例没有描述一个Java的动态代理类,则它的基本形成格式为:类名、serialVersionUID、标记、成员属性的数量,而基于类的本身,它又包含下边的规则:

  • 对于一个不能序列化的类,它的成员属性数量0,既不设置SC_SERIALIZABLE标记也不设置SC_EXTERNALIZABLE标记;
  • 对于一个可序列化的类,会设置标记SC_SERIALIZABLE成员属性的数量统计了可序列化的成员的个数,后面跟着每一个可序列化成员的描述符,这些描述符写入的顺序遵循定义规范。针对基础类型数据描述符,在写入字段类型和字段名之前会先按照字段名本身进行排序,排序方式在字段名上使用String.compareTo方法比较,简单点,最终结果就是字典序
  • 对于一个使用了外部化的类,它的标记包括SC_EXTERNALIZABLE,并且其可序列化的成员数量0
  • 对于Enum枚举类型而言,它的标记包括SC_ENUM,并且它的可序列化的成员数量0

  如果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_INTERFACEACC_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_ABSTRACTACC_STRICT标记。名称和描述符使用java.io.DataOutputStream中的writeUTF方法中的格式写入。
  字节流中的项的顺序如下:

  1. 类名称
  2. 32-bit整数表示类的修饰符
  3. 按照字典序排列的所有接口的接口名称
  4. 针对类中定义的每一个字段按照字典序【除开private staticprivate transient
    a.字段名称
    b.32-bit的整数表示字段的修饰符
    c.该字段的描述符
  5. 如果一个类定义中存在初始化操作,按照下边方式写入:
    a.初始化方法的名称,<clinit>
    b.当前方法的修饰符,写入32-bit整数java.lang.reflect.Modifier.STATIC
    c.方法的描述符V()
  6. 针对非私有(non-private)构造函数按照方法名和方法签名排序
    a.方法的名称<init>
    b.32-bit的整数表示方法的修饰符
    c.方法的描述符
  7. 针对非私有的方法按照方法名和方法签名排序
    a.方法的名称
    b.32-bit的整数表示方法的修饰符
    c.方法的描述符
  8. SHA-1的算法会执行这个字节流,这个字节流通过DataOutputStream生产,并且生产5个32bit的数值sha [0..4]
  9. 这个Hash值从SHA-1消息摘要【message digest中的第一位和第二位32-bit的整数中组装。第8步的结果会返回一个名为sha的数组,它包含了5个32-bit的字H0 H1 H2 H3 H4,这个hash值最终计算代码如下:

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)目标

  • 不同的JVM虚拟机中能够支持不同版本的类操作进行双向通信
    -- 定义了一种机制允许Java的类读取被旧版本相同的类写入到字节流的数据
    -- 定义了一种机制允许Java的类写入数据到字节流,其目的是为了让旧版本的类来读取
  • 持久化【Persistence】和远程访问【RMI提供默认的序列化机制;
  • 为了在某些情况下表现良好以及生产简单的字节流,所以RMI也可以使用序列化;
  • 能够标识和加载相匹配的确切类,用于写入该字节流;
  • 针对没有版本的类保持比较低开销;
  • 使用字节流格式允许在不需要调用流中对象的特定方法而实现对字节流的遍历操作

  2)假设注意这里使用了类进化概念”evolve“进化过的类可表示”新版本“类,未进化过的类可表示”旧版本“

  • 从可序列化的类必须控制字节流格式以完成目标开始,类的版本控制就应该适用于它。外部化的类会使用特定的外部化格式,所以它必须负责自己的版本控制
  • 所有的数据和对象在被读取或者跳过不读时,字节流的最终读取顺序应该和写入的时候相同
  • 一个类可以单独进化,就和它能和子类、父类一起正确进化一样;
  • 类可以使用名称进行标识,两个同名的类有可能是不同的版本,也可能是两个完全不同的类,这两个类可以通过比较接口或者接口的哈希值区分;
  • 默认的序列化不执行任何类型转换
  • 字节流格式仅仅需要支持顺序的类型变化不支持任意带分支的类型变化

  3)谁负责字节流的版本?

  在类的不断演化过程中,进化过的类【最新版本的职责是维护它和未进化的类【旧版本类之间设置的合约。这个有两种形式:
  第一,进化过的类禁止打破和原始版本提供的接口相关的存在假设,这样进化过的类就可以取代原始的类;
  第二,当进化过的类和原始版本【早期版本进行通信时,它必须提供足够和等效的信息,使其能够继续满足和没有未进化的类【旧版本之间的合约;


  和上边讨论的目的一样,每一个类需要实现和继承于某个接口或者和父类之间定义的合约。新版的类,例如图中的foo',必须满足foo拥有的合约而且它有可能实现某个接口而修改其实现。通过序列化在对象之间进行通信并不属于这些接口定义的合约,序列化是各种实现之间的一个私有协议,它的责任是使得所有的实现能够有效地通信,同时允许每一个实现继续满足其客户端期望的合约这里把和某个对象通信的另一端称为其客户端

  4)兼容的Java类型演化

  Java的语言规范讨论了Java中的类在进化过程其二进制码的兼容性,大部分二进制码的兼容性来自于Java中类、接口、字段、方法符号引用延迟绑定
  下边是设计Java中可序列化的对象流的原则:

  • 默认的序列化机制将使用符号模型,这种模型主要针对字节流中成员属性虚拟机中的Java类里的成员属性
  • 字节流中的每一个被引用的类将能唯一标识它自己、它的超类、它的类型它写入字节流的每一个字段的名称。这些字段的排序规则:使用字典序排列所有基础类型的字段,紧跟着使用字典序排列所有对象类型的字段;
  • 每一个类在字节流中会有两种不同的数据类型:必须数据【Required Data(对应对象中可以直接序列化的数据)可选数据【Optional Data(任意序列构成的基础数据和对象数据)。字节流格式定义了字节流中出现数据的必须属性和可选属性,这样的话若出现了跳过不写不读的情况可以直接跳过整个类、必须数据部分、可选数据部分
    -- 一个对象中字段的必须数据部分的组成顺序是由类描述符定义的;
    -- 可选数据部分在写入字节流的时候和类中的成员属性并不直接对应,类定义负责描述这些数据的长度、类型、以及可选信息的版本号
  • 如果一个类中定义了writeObject/readObject方法,则这个方法将会取代默认序列化机制中的方法写入/读取对象的状态,可选的信息可依靠这些方法写入或者读取,而必须数据部分可依赖defaultWriteObject方法和defaultReadObject方法;
  • 字节流格式用来标识每一个类的方式是使用SUID(Stream Unique Identifier,默认情况下该值是类的哈希值。所有最新版本的类都必须定义这些类能兼容的SUID,这样可以防止同名类出现,如同单个类的版本一样随意标识也不会混淆;
  • ObjectOutputStreamObjectInputStream的子类可以包含它们拥有的信息使用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对象

  一个类的ObjectStreamClass对象描述动态代理类按照下边规则:

  • 可兼容类的SUID【Stream Unique Identifier
  • 标记集合用来指示一个类的各种属性,包括该类是否定义了writeObject方法、是否可序列化、是否是可外部化的、是否是枚举Enum类型
  • 可序列化的字段的数量
  • 一个类的字段数组会被默认的序列化机制序列化,对于数组和对象的字段而言,字段的类型会包含在一个描述字符串中,它必须是“字段描述符【field descriptor】”格式(类似"Ljava/lang/Object;"),这个格式在虚拟机规范中有说明;
  • 可选的数据块记录【Data-Block和使用annotateClass方法写入字节流的对象;
  • 当前对象的超类的ObjectStreamClass(如果超类不可序列化则返回null);

  一个类的ObjectStreamClass对象描述动态代理类时按照下边规则:

  • 动态代理类实现的接口的数量;
  • 所有动态代理类实现的接口的名称列表,这些接口通过调用Class的getInterfaces方法的返回结果进行排序;
  • 可选的数据块记录【Data-Block和使用annotateProxyClass方法写入字节流的对象;
  • 当前对象的超类对应的ObjectStreamClassjava.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字符串使用的哪种格式。
  数组类型的数据描述信息包含下边内容:

  • 它的ObjectStreamClass对象;
  • 数组中元素的数量
  • 元素的值序列,这些值的类型会在数组类型中隐式转换,例如byte数组的元素是byte类型

  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方法会接收一个参数以表示写入的可序列化字节流的协议版本
  使用的字节流协议版本如下:

  • ObjectStreamConstants.PROTOCOL_VERSION_1:表示最初序列化字节流的格式;
  • ObjectStreamConstants.PROTOCOL_VERSION_2:表示新的外部字节流格式,基础类型的数据将会使用数据块【Data-Block的模式写入字节流,它以标记TC_ENDBLOCKDATA结束

  数据块的边界是标准化的,使用数据块模式写入字节流的基础类型的数据通常不能超过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_VERSIONcontents表示最终生成的序列的内容;

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中的内建序列化。



  • 14
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

戒子猪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值