4.1 ObjectStreamClass类
ObjectStreamClass
提供有关保存在序列化流中的类的信息。描述符提供类的完全限定名称及其序列化版本UID。 SerialVersionUID
标识了此类能够写入流并从中读取的唯一原始类版本。
package java.io;
public class ObjectStreamClass implements Serializable
{
public static ObjectStreamClass lookup(Class<?> cl);
public static ObjectStreamClass lookupAny(Class<?> cl);
public String getName();
public Class<?> forClass();
public ObjectStreamField[] getFields();
public ObjectStreamField getField(String name);
public long getSerialVersionUID();
public String toString();
}
lookup
方法返回虚拟机中指定类的ObjectStreamClass
描述符。如果类已定义serialVersionUID
,则从类中检索它。如果类未定义serialVersionUID
,则从虚拟机中类的定义计算它。如果指定的类不可序列化或外部化,则返回null。
lookupAny
方法的行为类似于lookup
方法,只是它返回任何类的描述符,无论它是否实现Serializable
。未实现Serializable
的类的serialVersionUID
为0L。
getName
方法返回类的名称,格式与Class.getName
方法使用的格式相同。
forClass
方法返回本地虚拟机中的Class
,如果被ObjectInputStream.resolveClass
方法找到。否则返回null。
getFields
方法返回表示此类的可序列化字段的ObjectStreamField
对象数组。
getSerialVersionUID
方法返回此类的serialVersionUID
。请参阅第4.6节,“流唯一标识符”。如果类未指定,返回的值是使用国家标准局定义的安全哈希算法(SHA)从类的名称、接口、方法和字段计算的哈希。
toString
方法返回类描述符的可打印表示,包括类的名称和serialVersionUID
。
4.2 动态代理类描述符
ObjectStreamClass描述符还用于提供有关保存在序列化流中的动态代理类(例如,通过调用java.lang.reflect.Proxy的getProxyClass方法获得的类)的信息。动态代理类本身没有可序列化字段,且serialVersionUID
为0L。换句话说,当将动态代理类的Class对象传递给ObjectStreamClass的静态lookup方法时,返回的ObjectStreamClass实例将具有以下属性:
- 调用其getSerialVersionUID方法将返回0L。
- 调用其getFields方法将返回长度为零的数组。
- 使用任何String参数调用其getField方法将返回null。
4.3 序列化形式
ObjectStreamClass实例的序列化形式取决于它所代表的Class对象是否可序列化、可外部化或动态代理类。
当将不代表动态代理类的ObjectStreamClass实例写入流时,它会写入类名和serialVersionUID
、标志和字段数。根据类的不同,可能会写入其他信息:
-
对于不可序列化的类,字段数始终为零。既不设置
SC_SERIALIZABLE
标志位,也不设置SC_EXTERNALIZABLE
标志位。 -
对于可序列化的类,设置
SC_SERIALIZABLE
标志位,字段数计算可序列化字段的数量,然后是每个可序列化字段的描述符。描述符按规范顺序编写。首先按字段名称排序原始类型字段的描述符,然后按字段名称排序对象类型字段的描述符。名称使用String.compareTo
排序。有关格式的详细信息,请参阅第6.4节,“流格式的语法”。 -
对于可外部化的类,标志包括
SC_EXTERNALIZABLE
标志位,字段数始终为零。 -
对于枚举类型,标志包括
SC_ENUM
标志位,字段数始终为零。
当ObjectOutputStream序列化动态代理类的ObjectStreamClass描述符时,通过将其Class对象传递给java.lang.reflect.Proxy的isProxyClass方法确定,它会写入动态代理类实现的接口数,然后是接口名称。接口按调用动态代理类的Class对象的getInterfaces方法返回的顺序列出。
动态代理类和非动态代理类的ObjectStreamClass描述符的序列化表示通过不同的类型码(TC_PROXYCLASSDESC
和TC_CLASSDESC
)进行区分;有关语法的更详细规范,请参见第6.4节,“流格式的语法”。
4.4 ObjectStreamField类
ObjectStreamField
表示可序列化类的可序列化字段。类的可序列化字段可以从ObjectStreamClass
中检索。
特殊的静态可序列化字段serialPersistentFields
是ObjectStreamField
组件数组,用于覆盖默认的可序列化字段。
package java.io;
public class ObjectStreamField implements Comparable<Object> {
public ObjectStreamField(String fieldName,
Class<?> fieldType);
public ObjectStreamField(String fieldName,
Class<?> fieldType,
boolean unshared);
public String getName();
public Class<?> getType();
public String getTypeString();
public char getTypeCode();
public boolean isPrimitive();
public boolean isUnshared();
public int getOffset();
protected void setOffset(int offset);
public int compareTo(Object obj);
public String toString();
}
ObjectStreamField
对象用于指定类的可序列化字段或描述流中存在的字段。其构造函数接受描述要表示的字段的参数:指定字段名称的字符串、指定字段类型的Class
对象以及指示是否应将表示字段的值读取和写入为“unshared”对象的boolean
标志(对于两参数构造函数隐式为false
),如果默认的序列化/反序列化正在使用(请参阅第3.1节,“ObjectInputStream类”和第2.1节,“ObjectOutputStream类”中的ObjectInputStream.readUnshared
和ObjectOutputStream.writeUnshared
方法的描述)。
getName
方法返回可序列化字段的名称。
getType
方法返回字段的类型。
getTypeString
方法返回字段的类型签名。
getTypeCode
方法返回字段类型的字符编码('B
'表示byte
,'C
'表示char
,'D
'表示double
,'F
'表示float
,'I
'表示int
,'J
'表示long
,'L
'表示非数组对象类型,'S
'表示short
,'Z
'表示boolean
,'[
'表示数组)。
isPrimitive
方法如果字段是原始类型,则返回true
,否则返回false
。
isUnshared
方法如果字段的值应写入为“unshared”对象,则返回true
,否则返回false
。
getOffset
方法返回字段值在定义字段的类的实例数据中的偏移量。
setOffset
方法允许ObjectStreamField
子类修改getOffset
方法返回的偏移值。
compareTo
方法比较ObjectStreamFields
以便排序使用。原始字段被排在非原始字段之前;否则相等的字段按字母顺序排列。
toString
方法返回带有名称和类型的可打印表示。
4.5 检查可序列化类
程序serialver可用于查找类是否可序列化并获取其serialVersionUID
。
在命令行上调用时,使用一个或多个类名,serialver会打印每个类的serialVersionUID
,以便复制到正在演变的类中。当不带参数调用时,它会打印一个用法行。
4.6 流唯一标识符
每个有版本的类必须标识其能够写入流并从中读取的原始类版本。例如,版本化类必须声明:
private static final long serialVersionUID = 3487495895819393L;
流唯一标识符是类名、接口类名、方法和字段的64位哈希值。该值必须在类的所有版本中声明,除了第一个版本。它可以在原始类中声明,但不是必需的。对于所有兼容的类,该值是固定的。如果一个类没有声明SUID,则该值默认为该类的哈希值。动态代理类和枚举类型的serialVersionUID
始终具有值0L。数组类不能声明显式的serialVersionUID
,因此它们始终具有默认计算值,但是对于数组类,匹配serialVersionUID
值的要求被豁免。记录类具有默认的serialVersionUID
值为0L
,但可以声明显式的serialVersionUID
。记录类的匹配serialVersionUID
值的要求也被豁免。
注意:强烈建议所有可序列化的类明确声明serialVersionUID
值,因为默认的serialVersionUID
计算对可能因编译器实现而异的类细节非常敏感,可能导致在反序列化过程中出现意外的serialVersionUID
冲突,导致反序列化失败。
Externalizable
类的初始版本必须输出一个未来可扩展的流数据格式。方法readExternal
的初始版本必须能够读取所有未来版本的方法writeExternal
的输出格式。
serialVersionUID
是使用反映类定义的字节流的签名计算的。使用国家标准与技术研究所(NIST)安全哈希算法(SHA-1)来为流计算签名。前两个32位数量用于形成64位哈希。使用java.lang.DataOutputStream
将基本数据类型转换为字节序列。输入到流中的值由Java虚拟机(VM)规范为类定义。类修饰符可能包括ACC_PUBLIC
、ACC_FINAL
、ACC_INTERFACE
和ACC_ABSTRACT
标志;其他标志将被忽略,不会影响serialVersionUID
的计算。同样,对于字段修饰符,只有ACC_PUBLIC
、ACC_PRIVATE
、ACC_PROTECTED
、ACC_STATIC
、ACC_FINAL
、ACC_VOLATILE
和ACC_TRANSIENT
标志在计算serialVersionUID
值时被使用。对于构造函数和方法修饰符,只有ACC_PUBLIC
、ACC_PRIVATE
、ACC_PROTECTED
、ACC_STATIC
、ACC_FINAL
、ACC_SYNCHRONIZED
、ACC_NATIVE
、ACC_ABSTRACT
和ACC_STRICT
标志被使用。名称和描述符以java.io.DataOutputStream.writeUTF
方法使用的格式写入。
流中的项目顺序如下:
-
类名。
-
类修饰符,以32位整数形式写入。
-
按名称排序的每个接口的名称。
-
按字段名排序的每个字段(除了
private static
和private transient
字段):-
字段的名称。
-
字段的修饰符,以32位整数形式写入。
-
字段的描述符。
-
-
如果存在类初始化器,则写出以下内容:
-
方法名,
<clinit>
。 -
方法的修饰符,
java.lang.reflect.Modifier.STATIC
,以32位整数形式写入。 -
方法的描述符,
()V
。
-
-
按方法名和签名排序的每个非
private
构造函数:-
方法名,
<init>
。 -
方法的修饰符,以32位整数形式写入。
-
方法的描述符。
-
-
按方法名和签名排序的每个非
private
方法:-
方法名。
-
方法的修饰符,以32位整数形式写入。
-
方法的描述符。
-
-
SHA-1算法在
DataOutputStream
生成的字节流上执行,并产生五个32位值sha[0..4]
。 -
哈希值由SHA-1消息摘要的前两个32位值组成。如果消息摘要的结果,即五个32位字
H0 H1 H2 H3 H4
,在名为sha
的五个int
值数组中,则哈希值将计算如下:
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;