Java Object Serialization Specification: 4 - Class Descriptors


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的类的serialVersionUID0L。

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实例将具有以下属性:

4.3 序列化形式

ObjectStreamClass实例的序列化形式取决于它所代表的Class对象是否可序列化、可外部化或动态代理类。

当将不代表动态代理类的ObjectStreamClass实例写入流时,它会写入类名和serialVersionUID、标志和字段数。根据类的不同,可能会写入其他信息:

当ObjectOutputStream序列化动态代理类的ObjectStreamClass描述符时,通过将其Class对象传递给java.lang.reflect.Proxy的isProxyClass方法确定,它会写入动态代理类实现的接口数,然后是接口名称。接口按调用动态代理类的Class对象的getInterfaces方法返回的顺序列出。

动态代理类和非动态代理类的ObjectStreamClass描述符的序列化表示通过不同的类型码(TC_PROXYCLASSDESCTC_CLASSDESC)进行区分;有关语法的更详细规范,请参见第6.4节,“流格式的语法”

4.4 ObjectStreamField类

ObjectStreamField表示可序列化类的可序列化字段。类的可序列化字段可以从ObjectStreamClass中检索。

特殊的静态可序列化字段serialPersistentFieldsObjectStreamField组件数组,用于覆盖默认的可序列化字段。

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.readUnsharedObjectOutputStream.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_PUBLICACC_FINALACC_INTERFACEACC_ABSTRACT标志;其他标志将被忽略,不会影响serialVersionUID的计算。同样,对于字段修饰符,只有ACC_PUBLICACC_PRIVATEACC_PROTECTEDACC_STATICACC_FINALACC_VOLATILEACC_TRANSIENT标志在计算serialVersionUID值时被使用。对于构造函数和方法修饰符,只有ACC_PUBLICACC_PRIVATEACC_PROTECTEDACC_STATICACC_FINALACC_SYNCHRONIZEDACC_NATIVEACC_ABSTRACTACC_STRICT标志被使用。名称和描述符以java.io.DataOutputStream.writeUTF方法使用的格式写入。

流中的项目顺序如下:

  1. 类名。

  2. 类修饰符,以32位整数形式写入。

  3. 按名称排序的每个接口的名称。

  4. 按字段名排序的每个字段(除了private staticprivate transient字段):

    1. 字段的名称。

    2. 字段的修饰符,以32位整数形式写入。

    3. 字段的描述符。

  5. 如果存在类初始化器,则写出以下内容:

    1. 方法名,<clinit>

    2. 方法的修饰符,java.lang.reflect.Modifier.STATIC,以32位整数形式写入。

    3. 方法的描述符,()V

  6. 按方法名和签名排序的每个非private构造函数:

    1. 方法名,<init>

    2. 方法的修饰符,以32位整数形式写入。

    3. 方法的描述符。

  7. 按方法名和签名排序的每个非private方法:

    1. 方法名。

    2. 方法的修饰符,以32位整数形式写入。

    3. 方法的描述符。

  8. SHA-1算法在DataOutputStream生成的字节流上执行,并产生五个32位值sha[0..4]

  9. 哈希值由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;