Java Object Serialization Specification: 1 - System Architecture


1.1 概述

存储和检索Java对象的能力对于构建除了最短暂的应用程序之外的所有应用程序都是必不可少的。以序列化形式存储和检索对象的关键在于表示对象的状态足以重建对象。要保存在流中的对象可以支持SerializableExternalizable接口。对于Java对象,序列化形式必须能够识别和验证对象内容保存自哪个Java类,并将内容恢复到新实例。对于可序列化对象,流包含足够的信息以将流中的字段恢复到类的兼容版本。对于Externalizable对象,类完全负责其内容的外部格式。

要存储和检索的对象通常引用其他对象。这些其他对象必须同时存储和检索,以保持对象之间的关系。当存储对象时,从该对象可达的所有对象也会被存储。

序列化Java对象的目标是:

1.2 向对象流写入数据

向流中写入对象和基本类型是一个简单的过程。例如:

// 将今天的日期序列化到文件中。
    FileOutputStream f = new FileOutputStream("tmp");
    ObjectOutput s = new ObjectOutputStream(f);
    s.writeObject("Today");
    s.writeObject(new Date());
    s.flush();

首先需要一个OutputStream,在这种情况下是一个FileOutputStream,用于接收字节。然后创建一个写入FileOutputStreamObjectOutputStream。接下来,将字符串"Today"和一个Date对象写入流中。一般来说,对象使用writeObject方法写入,基本类型使用DataOutput的方法写入流中。

writeObject方法(参见第2.3节,“writeObject方法”)序列化指定的对象,并递归地遍历其引用的其他对象,以创建对象图的完整序列化表示。在流中,对任何对象的第一个引用导致该对象被序列化或外部化,并为该对象分配一个句柄。对该对象的后续引用被编码为该句柄。使用句柄保留了对象图中自然发生的对象共享,并允许对象之间存在循环引用(即图中的循环)。

数组、枚举常量和类型为ClassObjectStreamClassString的对象需要特殊处理。其他对象必须实现SerializableExternalizable接口才能保存或从流中恢复。

基本数据类型使用DataOutput接口的方法写入流中,例如writeIntwriteFloatwriteUTF。单个字节和字节数组使用OutputStream的方法写入。除了可序列化字段外,基本数据以块数据记录的形式写入流中,每个记录前缀带有标记和记录中字节的数量指示。

ObjectOutputStream可以扩展以自定义流中关于类的信息或替换要序列化的对象。有关详细信息,请参阅annotateClassreplaceObject方法的描述。

1.3 从对象流读取数据

从流中读取对象,与写入一样,是一个简单的过程:

// 从文件中反序列化字符串和日期。
    FileInputStream in = new FileInputStream("tmp");
    ObjectInputStream s = new ObjectInputStream(in);
    String today = (String)s.readObject();
    Date date = (Date)s.readObject();

首先需要一个InputStream,在这种情况下是一个FileInputStream,作为源流。然后创建一个从InputStream读取的ObjectInputStream。接下来,从流中读取字符串"Today"和一个Date对象。一般来说,对象使用readObject方法读取,基本类型从流中读取使用DataInput的方法。

readObject方法反序列化流中的下一个对象,并递归地遍历其引用的其他对象,以创建序列化对象的完整图。

基本数据类型使用DataInput接口的方法从流中读取,例如readIntreadFloatreadUTF。单个字节和字节数组使用InputStream的方法读取。除了可序列化字段外,基本数据从块数据记录中读取。

ObjectInputStream可以扩展以利用流中关于类的自定义信息或替换已反序列化的对象。有关详细信息,请参阅resolveClassresolveObject方法的描述。

1.4 对象流作为容器

对象序列化生成和消耗包含一个或多个基本类型和对象的字节流。写入流的对象反过来引用其他对象,这些对象也在流中表示。对象序列化生成仅一种流格式,编码和存储包含的对象。

每个充当容器的对象都实现一个接口,该接口允许将基本类型和对象存储在其中或从其中检索。这些接口是ObjectOutputObjectInput接口,它们:

要存储在对象流中的每个对象必须显式允许自己被存储,并且必须实现保存和恢复其状态所需的协议。对象序列化定义了两种这样的协议。这些协议允许容器要求对象写入和读取其状态。

要存储在对象流中,每个对象必须实现SerializableExternalizable接口之一:

1.5 为类定义可序列化字段

类的可序列化字段可以通过两种不同的方式定义。类的默认可序列化字段被定义为非瞬态和非静态字段。可以通过在Serializable类中声明一个特殊字段serialPersistentFields来覆盖此默认计算。此字段必须用ObjectStreamField对象数组初始化,列出可序列化字段的名称和类型。字段的修饰符必须是private、static和final。如果字段的值为null或者不是ObjectStreamField[]的实例,或者字段没有所需的修饰符,则行为就像根本没有声明该字段一样。

例如,以下声明复制了默认行为。

class List implements Serializable {
    List next;

    private static final ObjectStreamField[] serialPersistentFields
                 = {new ObjectStreamField("next", List.class)};

}

通过使用serialPersistentFields为类定义可序列化字段,不再存在可序列化字段必须是Serializable类当前定义内的字段的限制。Serializable类的writeObjectreadObject方法可以使用描述在第1.7节,“访问类的可序列化字段”中描述的接口,将类的当前实现映射到类的可序列化字段。因此,Serializable类的字段可以在后续版本中更改,只要它保持回到其可序列化字段的映射,这些字段必须跨版本边界保持兼容。

注意:然而,使用此机制指定内部类的可序列化字段存在限制。内部类只能包含初始化为常量或从常量构建的表达式的最终静态字段。因此,不可能为内部类设置serialPersistentFields(尽管可以为静态成员类设置)。有关序列化内部类实例的其他限制,请参见第1.10节,“Serializable接口”

1.6 为类记录可序列化字段和数据

重要的是记录类的可序列化状态,以便与可替代的Serializable类实现进行互操作,并记录类的演变。记录可序列化字段为人提供了最后一次机会来审查该字段是否应该是可序列化的。序列化javadoc标签@serial@serialField@serialData提供了一种在源代码中记录Serializable类的序列化形式的方式。

javadoc应用程序识别序列化javadoc标签,并为每个Serializable和Externalizable类生成规范。有关使用这些标签的示例,请参见第C.1节,“java.io.File的替代实现示例”

当声明一个类为Serializable时,对象的可序列化状态由可序列化字段(按名称和类型)加上可选数据定义。可选数据只能由Serializable类的writeObject方法显式写入。可选数据可以由Serializable类的readObject方法读取,或者序列化将跳过未读取的可选数据。

当声明一个类为Externalizable时,由类本身写入流的数据定义了序列化状态。类必须指定写入流的每个数据的顺序、类型和含义。类必须处理自身的演变,以便可以继续读取先前版本写入的数据并写入可以被先前版本读取的数据。类必须在保存和恢复数据时与超类协调。必须指定超类数据在流中的位置。

Serializable类的设计者必须确保为类保存的信息适合持久性,并遵循序列化指定的互操作性和演变规则。类演变在第5章,“可序列化对象的版本控制”中有更详细的解释。

1.7 访问类的可序列化字段

序列化提供了两种访问流中可序列化字段的机制:

当读取或写入实现Serializable接口的对象且不进行进一步自定义时,默认机制会自动使用。可序列化字段映射到类的相应字段,并相应地将值写入流中或读取并分配值。如果类提供了writeObjectreadObject方法,则可以通过调用defaultWriteObjectdefaultReadObject来调用默认机制。当实现writeObjectreadObject方法时,类有机会在写入或读取可序列化字段值之前或之后修改这些值。

当无法使用默认机制时,可序列化类可以使用ObjectOutputStreamputFields方法将可序列化字段的值放入流中。ObjectOutputStreamwriteFields方法按正确顺序放置值,然后使用现有的序列化协议将其写入流。相应地,ObjectInputStreamreadFields方法从流中读取值,并按名称以任何顺序提供给类。有关Serializable Fields API的详细描述,请参见第2.2节,“ObjectOutputStream.PutField类”第3.2节,“ObjectInputStream.GetField类”

1.8 ObjectOutput接口

ObjectOutput接口提供了一个抽象的基于流的对象存储接口。它扩展了DataOutput接口,因此这些方法可用于写入原始数据类型。实现此接口的对象可用于存储基本类型和对象。

package java.io;

public interface ObjectOutput extends DataOutput
{
    public void writeObject(Object obj) throws IOException;
    public void write(int b) throws IOException;
    public void write(byte b[]) throws IOException;
    public void write(byte b[], int off, int len) throws IOException;
    public void flush() throws IOException;
    public void close() throws IOException;
}

writeObject方法用于写入对象。抛出的异常反映了访问对象或其字段时出现的错误,或在写入存储时发生的异常。如果抛出任何异常,底层存储可能会损坏。如果发生这种情况,请参考实现此接口的对象以获取更多信息。

1.9 ObjectInput接口

ObjectInput接口提供了一个抽象的基于流的对象检索接口。它扩展了DataInput接口,因此在此接口中可以访问用于读取原始数据类型的方法。

package java.io;

public interface ObjectInput extends DataInput
{
    public Object readObject()
        throws ClassNotFoundException, IOException;
    public int read() throws IOException;
    public int read(byte b[]) throws IOException;
    public int read(byte b[], int off, int len) throws IOException;
    public long skip(long n) throws IOException;
    public int available() throws IOException;
    public void close() throws IOException;
}

readObject方法用于读取并返回对象。抛出的异常反映了访问对象或其字段时出现的错误,或在从存储中读取时发生的异常。如果抛出任何异常,底层存储可能会损坏。如果发生这种情况,请参考实现此接口的对象以获取更多信息。

1.10 Serializable接口

对象序列化生成包含正在保存的对象的JavaTM类信息的流。对于可序列化对象,保留了足够的信息以便即使存在不同(但兼容)版本的类实现,也可以还原这些对象。Serializable接口被定义为标识实现可序列化协议的类:

package java.io;

public interface Serializable {};

可序列化类必须执行以下操作:

类可以选择定义以下方法:

ObjectOutputStreamObjectInputStream允许它们操作的可序列化类演变(允许对与先前版本兼容的类进行更改)。有关用于允许兼容更改的机制的信息,请参见第5.5节,“兼容的Java类型演变”

注意:强烈建议不要序列化内部类(即,非静态成员内部类,包括局部类和匿名类)。因为在非静态上下文中声明的内部类包含对外部类实例的隐式非瞬态引用,因此序列化这样的内部类实例将导致其关联的外部类实例的序列化。由javac(或其他JavaTM编译器)生成的用于实现内部类的合成字段是依赖于实现的,并且在编译器之间可能有所不同;这些字段的差异可能会破坏兼容性,也可能导致冲突的默认serialVersionUID值。分配给局部和匿名内部类的名称也是依赖于实现的,并且可能在编译器之间有所不同。由于内部类不能声明除编译时常量字段之外的静态成员,因此它们无法使用serialPersistentFields机制来指定可序列化字段。最后,由于与外部实例关联的内部类没有零参数构造函数(这样的内部类的构造函数隐式接受封闭实例作为前置参数),它们无法实现Externalizable。然而,上述问题都不适用于静态成员类。

1.11 Externalizable接口

对于Externalizable对象,容器仅保存对象的类的标识;类必须保存和恢复内容。Externalizable接口定义如下:

package java.io;

public interface Externalizable extends Serializable
{
    public void writeExternal(ObjectOutput out)
        throws IOException;

    public void readExternal(ObjectInput in)
        throws IOException, java.lang.ClassNotFoundException;
}

Externalizable对象的类必须执行以下操作:

Externalizable类可以选择定义以下方法:

1.12 枚举常量的序列化

枚举常量的序列化方式与普通可序列化或可外部化对象不同。枚举常量的序列化形式仅包含其名称;常量的字段值不在形式中。为了序列化枚举常量,ObjectOutputStream写入枚举常量的name方法返回的值。为了反序列化枚举常量,ObjectInputStream从流中读取常量名称;然后通过调用java.lang.Enum.valueOf方法,传递常量的枚举类型和接收到的常量名称作为参数来获取反序列化的常量。与其他可序列化或可外部化对象一样,枚举常量可以作为后续出现在序列化流中的反向引用的目标。

枚举常量的序列化过程无法定制:枚举类型定义的任何特定于类的writeObjectreadObjectreadObjectNoDatawriteReplacereadResolve方法在序列化和反序列化过程中将被忽略。同样,任何serialPersistentFieldsserialVersionUID字段声明也将被忽略--所有枚举类型都具有固定的serialVersionUID0L。对于枚举类型的可序列化字段和数据进行文档化是不必要的,因为发送的数据类型没有变化。

1.13 记录的序列化

记录的序列化方式与普通可序列化或可外部化对象不同。记录对象的序列化形式是从记录组件派生的值序列。记录对象的流格式与流中的普通对象相同。在反序列化过程中,如果指定流类描述符的本地类等效于记录类,则首先读取流字段并重建以用作记录的组件值;其次,通过使用组件值作为参数(或者如果流中缺少组件值,则使用组件类型的默认值)调用记录的规范构造函数来创建记录对象。

与其他可序列化或可外部化对象一样,记录对象可以作为后续出现在序列化流中的反向引用的目标。然而,通过记录对象直接或间接地被其组件之一引用的图中的循环不会被保留。在调用记录构造函数之前,记录组件将被反序列化,因此这种限制存在(有关更多信息,请参见第1.14节,“循环引用”)。

记录对象的序列化或外部化过程无法定制;记录类定义的任何特定于类的writeObjectreadObjectreadObjectNoDatawriteExternalreadExternal方法在序列化和反序列化过程中将被忽略。但是,可以通过writeReplacereadResolve方法指定要序列化的替代对象或指定替换对象。任何serialPersistentFields字段声明都将被忽略。记录类的文档化可序列化字段和数据是不必要的,因为序列化形式中没有数据类型的变化,除非使用了替代或替换对象。记录类的serialVersionUID0L,除非显式声明。对于记录类,匹配的serialVersionUID值的要求被豁免。

1.14 循环引用

第1.2节,“写入对象流”中所述,使用句柄可以保留对象图中出现的循环引用。

一个仅用于说明目的的最小人为示例:

    class Data implements Serializable {
        private static final long serialVersionUID = ...
        Object obj;
    }

    class Carrier implements Serializable {
        private static final long serialVersionUID = ...
        private final Data d;
        public Carrier(Data d) { this.d = d; }
        public Data d() { return d; }
    }

    // 创建Data和Carrier的实例,并在它们之间创建一个循环
    Data d1 = new Data();
    Carrier c1 = new Carrier(d1);
    d1.obj = c1;

    // 序列化
    ObjectOutputStream oos = new ObjectOutputStream(...);
    oos.writeObject(c1);

    // 反序列化
    ObjectInputStream ois  = new ObjectInputStream(...);
    Carrier c2 = (Carrier) ois.readObject();

在反序列化时,对象 将其 字段引用到 的实例,然后将其 字段再次引用到相同的 实例。 引用的对象的标识等于 引用的对象的标识,即

对象 的分配和其句柄的赋值发生在其字段值重建之前(请参见 第3.1节,“ObjectInputStream类”,步骤12)。这允许字段值(及其递归地字段值)在反序列化过程中引用 的句柄。通过这种方式,普通对象的反序列化支持对象图中的循环。

现在考虑如果 是一个记录类,如下所示:

    record Carrier(Data d) implements Serializable { }

在反序列化时,对象 将其 字段引用到 的实例,然后将其 字段引用到 (而不是引用到 )。原始对象图中通过 的循环引用在反序列化过程中不会被保留。

记录对象 的分配和其句柄的赋值发生在其字段值重建之后(即,未来记录的组件值的组件值;请参见 第3.1节,“ObjectInputStream类”,步骤11)。虽然在记录组件值重建之前将记录对象的句柄添加到已知对象集中,但其初始值为 。只有在记录对象被构造(通过调用其规范构造函数)后,才将句柄分配给记录对象。因此,在记录组件值的反序列化过程中,流中对记录对象的句柄的引用将看到初始的 值。因此,在反序列化过程中,不会保留从其组件(或它们的字段递归地)指向记录对象的循环。

1.15 保护敏感信息

在开发提供对资源受控访问的类时,必须小心保护敏感信息和功能。在反序列化过程中,对象的私有状态将被恢复。例如,文件描述符包含一个句柄,提供对操作系统资源的访问。能够伪造文件描述符将允许某些形式的非法访问,因为从流中恢复状态。因此,序列化运行时必须采取保守的方法,不信任流仅包含对象的有效表示。为了避免损害类,对象的敏感状态不应从流中恢复,或者必须由类重新验证。有几种技术可用于保护类中的敏感数据。

最简单的技术是将包含敏感数据的字段标记为私有瞬态。瞬态字段不是持久的,不会被任何持久性机制保存。标记字段将防止状态出现在流中并在反序列化过程中被恢复。由于写入和读取(私有字段)不能在类外部被覆盖,类的瞬态字段是安全的。

特别敏感的类根本不应该被序列化。为了实现这一点,对象不应该实现SerializableExternalizable接口。

一些类可能发现允许写入和读取但专门处理和重新验证状态在反序列化时是有益的。类应该实现writeObjectreadObject方法,仅保存和恢复适当的状态。如果应拒绝访问,抛出NotSerializableException将阻止进一步访问。