6.1 概述
流格式满足以下设计目标:
- 紧凑且结构化,便于高效读取。
- 允许仅通过了解流的结构和格式跳过流。不需要调用任何特定类的代码。
- 仅需要对数据进行流访问。
6.2 流元素
需要基本结构来表示流中的对象。需要表示对象的每个属性:其类、其字段以及由类特定方法编写和后来读取的数据。可以用语法描述流中对象的表示。对于空对象、新对象、类、数组、字符串以及对流中已有对象的后向引用,都有特殊表示。写入流的每个对象都被分配一个句柄,用于引用该对象。句柄从0x7E0000开始按顺序分配。当流被重置时,句柄重新从0x7E0000开始。
类对象由以下表示:
- 其
ObjectStreamClass
对象。
对于不是动态代理类的类的ObjectStreamClass
对象由以下表示:
-
兼容类的流唯一标识符(SUID)。
-
指示类的各种属性的一组标志,例如类是否定义了
writeObject
方法,类是否可序列化、可外部化或是枚举类型 -
可序列化字段的数量
-
类的字段数组,这些字段由默认机制序列化对于数组和对象字段,字段的类型作为字符串包含在其中,该字符串必须符合“字段描述符”格式(例如,“
Ljava/lang/Object;
”),如Java虚拟机规范中所述。 -
由
annotateClass
方法编写的可选块数据记录或对象 -
其超类的
ObjectStreamClass
(如果超类不可序列化,则为null)
对于动态代理类的ObjectStreamClass
对象由以下表示:
-
动态代理类实现的接口数量
-
动态代理类实现的所有接口的名称,按照在Class对象上调用
getInterfaces
方法返回的顺序列出 -
由
annotateProxyClass
方法编写的可选块数据记录或对象 -
其超类的
java.lang.reflect.Proxy
的ObjectStreamClass
String
对象的表示包括长度信息,后跟使用修改后的UTF-8编码的字符串内容。修改后的UTF-8编码与Java虚拟机中使用的编码以及java.io.DataInput
和DataOutput
接口中使用的编码相同;它与标准UTF-8在表示补充字符和空字符方面有所不同。长度信息的形式取决于使用修改后的UTF-8编码的字符串的长度。如果给定String
的修改后的UTF-8编码长度不超过65536字节,则长度将写为表示无符号16位整数的2字节。从Java 2平台,标准版,v1.3开始,如果使用修改后的UTF-8编码的字符串的长度为65536字节或更多,则长度将写为表示有符号64位整数的8字节。在序列化流中在String
之前的类型码指示写入String
时使用的格式。
数组由以下表示:
-
它们的
ObjectStreamClass
对象。 -
元素数量。
-
值的序列。值的类型隐含在数组的类型中。例如,字节数组的值为字节类型。
枚举常量由以下表示:
-
常量的基本枚举类型的
ObjectStreamClass
对象。 -
常量的名称字符串。
流中的新对象由以下表示:
-
对象的最派生类。
-
对象的每个可序列化类的数据,最高超类优先。对于每个类,流包含以下内容:
-
可序列化字段。参见第1.5节,“为类定义可序列化字段”。
-
如果类具有
writeObject
/readObject
方法,则可能存在由writeObject
方法编写的可选对象和/或原始类型的块数据记录,后跟endBlockData
代码。
-
由类编写的所有原始数据都被缓冲并包装在块数据记录中,无论数据是在writeObject
方法内部写入流还是直接从writeObject
方法外部写入流。这些数据只能由相应的readObject
方法读取,或者直接从流中读取。由writeObject
方法编写的对象终止任何先前的块数据记录,并根据需要将其写入为常规对象、null或后向引用。块数据记录允许错误恢复以丢弃任何可选数据。当从类内部调用时,流可以丢弃任何数据或对象,直到endBlockData
。
6.3 流协议版本
在JDK 1.2中对序列化流格式进行了一项不向后兼容的更改,不适用于JDK 1.1的所有次要版本。为了提供需要向后兼容的情况,添加了一项功能,用于指示在写入序列化流时要使用哪个PROTOCOL_VERSION
。方法ObjectOutputStream.useProtocolVersion
以参数形式接受要用于写入序列化流的协议版本。
流协议版本如下:
-
ObjectStreamConstants.PROTOCOL_VERSION_1
:表示初始流格式。 -
ObjectStreamConstants.PROTOCOL_VERSION_2
:表示新的外部数据格式。原始数据以块数据模式写入,并以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
。
6.4 流格式的语法
下表包含流格式的语法。非终结符以斜体显示。终结符以固定宽度字体显示。非终结符的定义后跟一个“:”。定义后跟一个或多个备选项,每个备选项占据单独的一行。以下表描述了符号:
符号 | 含义 |
---|---|
(数据类型) | 此标记具有指定的数据类型,例如byte。 |
标记[n] | 标记的预定义数量出现,即数组。 |
x0001 | 以十六进制表示的文字值。十六进制数字的数量反映了值的大小。 |
<xxx> | 从流中读取的值,用于指示数组的长度。 |
请注意,符号(utf)用于指定使用2字节长度信息编写的字符串,而符号(long-utf)用于指定使用8字节长度信息编写的字符串。有关详细信息,请参阅第6.2节,“流元素”。
6.4.1 语法规则
序列化流由满足stream规则的任何流表示。
流:
魔法 版本 内容
内容:
内容
内容 内容
内容:
对象
块数据
对象:
新对象
新类
新数组
新字符串
新枚举
新类描述
前一个对象
空引用
异常
TC_RESET
新类:
TC_CLASS 类描述 新句柄
类描述:
新类描述
空引用
(ClassDesc)前一个对象 // 要求为ClassDesc类型的对象
超类描述:
类描述
新类描述:
TC_CLASSDESC 类名 序列化版本号 新句柄 类描述信息
TC_PROXYCLASSDESC 新句柄 代理类描述信息
类描述信息:
类描述标志 字段 类注解 超类描述
类名:
(utf)
序列化版本号:
(long)
类描述标志:
(byte) // 在终端符号和常量中定义
代理类描述信息:
(int)<数量> 代理接口名[数量] 类注解
超类描述
代理接口名:
(utf)
字段:
(short)<数量> 字段描述[数量]
字段描述:
基本描述
对象描述
基本描述:
基本类型码 字段名
对象描述:
对象类型码 字段名 类名1
字段名:
(utf)
类名1:
(String)对象 // 包含字段类型的字符串,
// 以字段描述符格式
类注解:
结束块数据
内容 结束块数据 // 由annotateClass编写的内容
基本类型码:
'B' // 字节
'C' // 字符
'D' // 双精度
'F' // 浮点
'I' // 整数
'J' // 长整型
'S' // 短整型
'Z' // 布尔型
对象类型码:
'[' // 数组
'L' // 对象
新数组:
TC_ARRAY 类描述 新句柄 (int)<大小> 值[大小]
新对象:
TC_OBJECT 类描述 新句柄 类数据[] // 每个类的数据
类数据:
无写类 // SC_SERIALIZABLE & 类描述标志 &&
// !(SC_WRITE_METHOD & 类描述标志)
有写类 对象注解 // SC_SERIALIZABLE & 类描述标志 &&
// SC_WRITE_METHOD & 类描述标志
外部内容 // SC_EXTERNALIZABLE & 类描述标志 &&
// !(SC_BLOCKDATA & 类描述标志
对象注解 // SC_EXTERNALIZABLE & 类描述标志&&
// SC_BLOCKDATA & 类描述标志
无写类:
值 // 按类描述符顺序的字段
有写类:
无写类
对象注解:
结束块数据
内容 结束块数据 // 由writeObject或writeExternal PROTOCOL_VERSION_2编写的内容
块数据:
短块数据
长块数据
短块数据:
TC_BLOCKDATA (无符号字节)<大小> (字节)[大小]
长块数据:
TC_BLOCKDATALONG (int)<大小> (字节)[大小]
结束块数据:
TC_ENDBLOCKDATA
外部内容: // 只能由readExternal解析
(字节) // 基本数据
对象
外部内容:
writeExternal中写入的外部内容 // PROTOCOL_VERSION_1中的外部内容
外部内容 writeExternal
新字符串:
TC_STRING 新句柄 (utf)
TC_LONGSTRING 新句柄 (long-utf)
新枚举:
TC_ENUM 类描述 新句柄 枚举常量名
枚举常量名:
(String)对象
前一个对象:
TC_REFERENCE (int)句柄
空引用:
TC_NULL
异常:
TC_EXCEPTION 重置 (Throwable)对象 重置
魔法:
STREAM_MAGIC
版本:
STREAM_VERSION
值: // 当前对象的类描述描述了大小和类型
新句柄: // 分配给正在序列化或反序列化的对象的下一个序号
重置: // 已知对象集被丢弃,因此异常的对象不会与先前发送的对象或在异常之后可能发送的对象重叠
6.4.2 终端符号和常量
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; //如果SC_SERIALIZABLE
final static byte SC_BLOCK_DATA = 0x08; //如果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
将Externalizable
类写入流中,则设置。默认情况下,这是JDK 1.2中用于将Externalizable
对象写入流的协议。JDK 1.1写入STREAM_PROTOCOL_1
。
标志 SC_SERIALIZABLE
如果写入流的类扩展了java.io.Serializable
但没有扩展java.io.Externalizable
,则读取流的类也必须扩展java.io.Serializable
,并且将使用默认的序列化机制。
标志 SC_EXTERNALIZABLE
如果写入流的类扩展了java.io.Externalizable
,则读取数据的类也必须扩展Externalizable
,并且将使用其writeExternal
和readExternal
方法读取数据。
标志 SC_ENUM
如果写入流的类是枚举类型,则设置。接收者的相应类也必须是枚举类型。枚举类型的常量数据将按照第1.12节,“枚举常量的序列化”中描述的方式写入和读取。
示例
考虑一个原始类和链表中的两个实例的情况:
class List implements java.io.Serializable {
int value;
List next;
public static void main(String[] args) {
try {
List list1 = new List();
List list2 = new List();
list1.value = 17;
list1.next = list2;
list2.value = 19;
list2.next = null;
ByteArrayOutputStream o = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(o);
out.writeObject(list1);
out.writeObject(list2);
out.flush();
...
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
生成的流包含:
00: ac ed 00 05 73 72 00 04 4c 69 73 74 69 c8 8a 15 >....sr..Listi...<
10: 40 16 ae 68 02 00 02 49 00 05 76 61 6c 75 65 4c >Z......I..valueL<
20: 00 04 6e 65 78 74 74 00 06 4c 4c 69 73 74 3b 78 >..nextt..LList;x<
30: 70 00 00 00 11 73 71 00 7e 00 00 00 00 00 13 70 >p....sq.~......p<
40: 71 00 7e 00 03 >q.~..<