Java 对象序列化规范:4 - 类描述符


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

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

4.3 Serialized Form

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

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

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

动态代理类和非动态代理类的 ObjectStreamClass 描述符的序列化表示通过使用不同的类型代码(分别为 TC_PROXYCLASSDESCTC_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.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 方法返回 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_PUBLICACC_FINALACC_INTERFACEACC_ABSTRACT 标志;其他标志被忽略,不影响 serialVersionUID 计算。同样,对于字段修饰符,在计算 serialVersionUID 值时仅使用 ACC_PUBLICACC_PRIVATEACC_PROTECTEDACC_STATICACC_FINALACC_VOLATILEACC_TRANSIENT 标志。对于构造函数和方法修饰符,仅使用 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;