4.1 对象流类
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
方法使用的格式相同。
如果 ObjectInputStream.resolveClass
方法找到了一个,则 forClass
方法返回本地虚拟机中的 Class
。否则,它返回 null 。
getFields
方法返回一个 ObjectStreamField
对象数组,表示此类的可序列化字段。
getSerialVersionUID
方法返回此类的 serialVersionUID
。请参阅 第 4.6 节,“流唯一标识符”。如果类未指定,则返回的值是使用美国国家标准协会定义的安全散列算法 (SHA) 从类的名称、接口、方法和字段计算得出的散列值。
toString
方法返回类描述符的可打印表示形式,包括类名和 serialVersionUID
。
4.2 动态代理类描述符
ObjectStreamClass 描述符还用于提供有关保存在序列化流中的动态代理类(例如,通过调用 java.lang.reflect.Proxy 的 getProxyClass 方法获得的类)的信息。动态代理类本身没有可序列化字段和 0L 的 serialVersionUID。也就是说,当一个动态代理类的Class对象被传递给ObjectStreamClass的静态查找方法时,返回的ObjectStreamClass实例将具有以下属性:
- 调用它的 getSerialVersionUID 方法将返回 0L。
- 调用它的 getFields 方法将返回一个长度为零的数组。
- 使用任何 String 参数调用其 getField 方法将返回 null。
4.3 Serialized Form
ObjectStreamClass 实例的序列化形式取决于它表示的 Class 对象是可序列化的、可外部化的还是动态代理类。
当不代表动态代理类的 ObjectStreamClass
实例写入流时,它会写入类名和 serialVersionUID
、标志和字段数。根据类别,可能会写入其他信息:
对于不可序列化的类,字段数始终为零。
SC_SERIALIZABLE
和SC_EXTERNALIZABLE
标志位均未设置。对于可序列化类,
SC_SERIALIZABLE
标志被设置,字段数计算可序列化字段的数量,后面是每个可序列化字段的描述符。描述符是按规范顺序编写的。原始类型字段的描述符首先按字段名排序,然后是对象类型字段的描述符,按字段名排序。名称使用String.compareTo
排序。有关格式的详细信息,请参阅第 6.4 节,“流格式的语法”。对于可外部化的类,flags 包括
SC_EXTERNALIZABLE
标志,并且字段数始终为零。对于枚举类型,flags 包括
SC_ENUM
标志,并且字段数始终为零。
当 ObjectOutputStream 序列化动态代理类的 ObjectStreamClass 描述符时,通过将其 Class 对象传递给 java.lang.reflect.Proxy 的 isProxyClass 方法来确定,它写入动态代理类实现的接口数,后跟接口名字。接口按照它们通过调用动态代理类的 Class 对象上的 getInterfaces 方法返回的顺序列出。
动态代理类和非动态代理类的 ObjectStreamClass 描述符的序列化表示通过使用不同的类型代码(分别为 TC_PROXYCLASSDESC
和 TC_CLASSDESC
)来区分;有关语法的更详细规范,请参阅 第 6.4 节,“流格式的语法” 。
4.4 对象流字段类
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
对象,以及指示如果正在使用默认序列化/反序列化,则表示的字段应作为“非共享”对象读取和写入(分别参见 第 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
方法返回 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
值的要求。
Note: 强烈建议所有可序列化类显式声明 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
计算。同样,对于字段修饰符,在计算 serialVersionUID
值时仅使用 ACC_PUBLIC
、ACC_PRIVATE
、ACC_PROTECTED
、ACC_STATIC
、ACC_FINAL
、ACC_VOLATILE
和 ACC_TRANSIENT
标志。对于构造函数和方法修饰符,仅使用 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;