Java Object Serialization Specification: 2 - Object Output Classes


2.1 ObjectOutputStream类

ObjectOutputStream实现对象序列化。它维护流的状态,包括已经序列化的对象集合。其方法控制要序列化的对象的遍历,以保存指定的对象及其引用的对象。

package java.io;

public class ObjectOutputStream
    extends OutputStream
    implements ObjectOutput, ObjectStreamConstants
{
    public ObjectOutputStream(OutputStream out)
        throws IOException;

    public final void writeObject(Object obj)
        throws IOException;

    public void writeUnshared(Object obj)
        throws IOException;

    public void defaultWriteObject()
        throws IOException, NotActiveException;

    public PutField putFields()
        throws IOException;

    public void writeFields()
        throws IOException;

    public void reset() throws IOException;

    protected void annotateClass(Class<?> cl) throws IOException;

    protected void annotateProxyClass(Class<?> cl) throws IOException;

    protected void writeClassDescriptor(ObjectStreamClass desc)
        throws IOException;

    protected Object replaceObject(Object obj) throws IOException;

    protected boolean enableReplaceObject(boolean enable)
        throws SecurityException;

    protected void writeStreamHeader() throws IOException;

    public void write(int data) 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;

    protected void drain() throws IOException;

    public void close() throws IOException;

    public void writeBoolean(boolean data) throws IOException;

    public void writeByte(int data) throws IOException;

    public void writeShort(int data) throws IOException;

    public void writeChar(int data) throws IOException;

    public void writeInt(int data) throws IOException;

    public void writeLong(long data) throws IOException;

    public void writeFloat(float data) throws IOException;

    public void writeDouble(double data) throws IOException;

    public void writeBytes(String data) throws IOException;

    public void writeChars(String data) throws IOException;

    public void writeUTF(String data) throws IOException;

    // Inner class to provide access to serializable fields.
    public abstract static class PutField
    {
        public void put(String name, boolean value)
            throws IllegalArgumentException;

        public void put(String name, char data)
            throws IllegalArgumentException;

        public void put(String name, byte data)
            throws IllegalArgumentException;

        public void put(String name, short data)
            throws IllegalArgumentException;

        public void put(String name, int data)
            throws IllegalArgumentException;

        public void put(String name, long data)
            throws IllegalArgumentException;

        public void put(String name, float data)
            throws IllegalArgumentException;

        public void put(String name, double data)
            throws IllegalArgumentException;

        public void put(String name, Object data)
            throws IllegalArgumentException;
    }

    public void useProtocolVersion(int version) throws IOException;

    protected ObjectOutputStream()
        throws IOException;

    protected writeObjectOverride()
        throws NotActiveException, IOException;
}

单参数ObjectOutputStream构造函数创建一个将对象序列化到给定OutputStreamObjectOutputStream。构造函数调用writeStreamHeader方法向流中写入一个魔数和版本,该魔数和版本将由单参数ObjectInputStream构造函数中的对应调用readStreamHeader来读取和验证。如果安装了安全管理器,则此构造函数在直接或间接被覆盖putFields和/或writeUnshared方法的子类的构造函数调用时,会检查"enableSubclassImplementation"SerializablePermission

writeObject方法用于将对象序列化到流中。对象的序列化如下:

  1. 如果子类正在覆盖实现,则调用writeObjectOverride方法并返回。覆盖实现在本节末尾有描述。

  2. 如果块数据缓冲区中有数据,则将数据写入流中并重置缓冲区。

  3. 如果对象为null,则将null放入流中,writeObject返回。

  4. 如果对象已被替换,如步骤8中描述的,将替换的句柄写入流中,writeObject返回。

  5. 如果对象已经写入流中,则将其句柄写入流中,writeObject返回。

  6. 如果对象为Class,则将相应的ObjectStreamClass写入流中,为该类分配一个句柄,writeObject返回。

  7. 如果对象为ObjectStreamClass,则为对象分配一个句柄,然后使用writeClassDescriptor方法之一将其写入流中,这些方法在Java 2 SDK标准版1.3及更高版本中被调用,如果它表示的类不是动态代理类,则通过将关联的Class对象传递给java.lang.reflect.ProxyisProxyClass方法来确定。然后,为所表示的类编写注释:如果类是动态代理类,则调用annotateProxyClass方法;否则,调用annotateClass方法。然后writeObject方法返回。

  8. 通过对象的类和/或ObjectInputStream的子类进行潜在替换处理。

    1. 如果对象的类不是枚举类型并定义了适当的writeReplace方法,则调用该方法。可选地,它可以返回要序列化的替代对象。

    2. 然后,通过调用enableReplaceObject方法启用,调用replaceObject方法允许ObjectOutputStream的子类替换要序列化的对象。如果原始对象在上一步中被替换,则使用替换对象调用replaceObject方法。

    如果原始对象被上述一步或两步替换,将原始对象到替换对象的映射记录下来,以便在步骤4中稍后使用。然后,重复步骤3到7处理新对象。

    如果替换对象不是步骤3到7覆盖的类型之一,则在步骤10中使用替换对象恢复处理。

  9. 如果对象为java.lang.String,则将字符串作为长度信息写入,后跟使用修改后的UTF-8编码的字符串内容。有关详细信息,请参阅第6.2节,“流元素”。为字符串分配一个句柄,writeObject返回。

  10. 如果对象为数组,则递归调用writeObject以写入数组的ObjectStreamClass。为数组分配一个句柄。然后写入数组的长度。然后将数组的每个元素写入流中,之后writeObject返回。

  11. 如果对象为枚举常量,则通过递归调用writeObject写入常量的枚举类型的ObjectStreamClass。它只会在第一次引用时出现在流中。为枚举常量分配一个句柄。接下来,通过枚举常量的name方法返回的值作为String对象写入,如第9步所述。请注意,如果相同的名称字符串先前出现在流中,则将写入对其的反向引用。然后writeObject方法返回。

  12. 如果对象为记录对象,则通过递归调用writeObject写入记录对象的类的ObjectStreamClass。它只会在第一次引用时出现在流中。为记录对象分配一个句柄。

    记录对象的组件写入流中。

     a.  如果记录对象是可序列化或可外部化的,则记录
         组件被写入,就好像调用`defaultWriteObject`
         方法一样。
    
     b.  如果对象既不是可序列化也不是可外部化,则
         抛出`NotSerializableException`。

    然后writeObject方法返回。

  13. 对于常规对象,通过递归调用writeObject写入对象的类的ObjectStreamClass。它只会在第一次引用时出现在流中。为对象分配一个句柄。

    对象的内容写入流中。

    a.  如果对象是可序列化的,则找到最高可序列化类。对于该类和每个派生类,将写入该类的字段。
        如果类没有`writeObject`方法,则调用
        `defaultWriteObject`方法将可序列化字段写入流中。
        如果类有`writeObject`方法,则调用该方法。
        它可以调用`defaultWriteObject`或`putFields`和
        `writeFields`来保存对象的状态,然后可以写入
        其他信息到流中。
    
    b.  如果对象是可外部化的,则调用对象的
        `writeExternal`方法。
    
    c.  如果对象既不是可序列化也不是可外部化,则
        抛出`NotSerializableException`。

在遍历过程中可能会发生异常,也可能会在底层流中发生异常。对于IOException的任何子类,异常将使用异常协议写入流中,并丢弃流状态。如果在尝试将第一个异常写入流时抛出第二个IOException,则流将处于未知状态,并且从writeObject中抛出StreamCorruptedException。对于其他异常,流将被中止,并处于未知且不可用的状态。

writeUnshared方法将一个“unshared”对象写入ObjectOutputStream。该方法与writeObject相同,只是它总是将给定对象作为流中的新对象写入(而不是指向先前序列化实例的后向引用)。具体来说:

通过writeUnshared写入对象本身并不能保证在反序列化时获得唯一引用,但它允许在流中多次定义单个对象,以便接收方通过多次调用ObjectInputStream.readUnshared方法(参见第3.1节,“ObjectInputStream类”)不会发生冲突。请注意,上述规则仅适用于使用writeUnshared写入的基本级别对象,而不适用于要序列化的对象图中的任何传递引用的子对象。

defaultWriteObject方法实现了当前类的默认序列化机制。此方法只能从类的writeObject方法中调用。该方法将当前类的所有可序列化字段写入流中。如果从writeObject方法之外调用,将抛出NotActiveException

putFields方法返回一个PutField对象,调用者用于设置流中可序列化字段的值。可以以任何顺序设置字段。设置完所有字段后,必须调用writeFields以按规范顺序将字段值写入流中。如果未设置字段,则将适当类型的默认值写入流中。此方法只能从可序列化类的writeObject方法中调用。它不能被多次调用,也不能在调用defaultWriteObject后调用。只有在调用writeFields后才能向流中写入其他数据。

reset方法将流状态重置为刚构造时的状态。Reset将丢弃已写入流中的任何对象的状态。流中的当前点被标记为重置,因此相应的ObjectInputStream将在相同点重置。先前写入流中的对象不会被记为已经写入流中。它们将再次写入流中。当必须再次发送对象的内容时,这很有用。在序列化对象时不能调用Reset。如果不当调用,将抛出IOException

从Java 2 SDK标准版v1.3开始,当需要序列化ObjectStreamClass时,将调用writeClassDescriptor方法。writeClassDescriptor负责将ObjectStreamClass的表示写入序列化流。子类可以重写此方法以自定义类描述符写入序列化流的方式。如果重写了此方法,则应该还重写ObjectInputStream中的相应readClassDescriptor方法,以从其自定义流表示中重建类描述符。默认情况下,writeClassDescriptor根据第6.4节,“流格式语法”中指定的格式写入类描述符。请注意,只有在ObjectOutputStream不使用旧的序列化流格式时才会调用此方法(参见第6.3节,“流协议版本”)。如果序列化流使用旧格式(ObjectStreamConstants.PROTOCOL_VERSION_1),则类描述符将以无法被覆盖或自定义的方式内部写入。

在序列化Class时,将调用annotateClass方法,并在类描述符写入流后调用。子类可以扩展此方法并向流中写入有关类的其他信息。此信息必须由相应的ObjectInputStream子类中的resolveClass方法读取。

ObjectOutputStream子类可以实现replaceObject方法来在序列化过程中监视或替换对象。替换对象必须通过在调用第一个要替换的对象的writeObject之前显式调用enableReplaceObject来启用。一旦启用,将在首次序列化对象之前为每个对象调用replaceObject。请注意,replaceObject方法不会用于特殊处理的类ClassObjectStreamClass的对象。子类的实现可以返回一个可序列化的替代对象,该对象将代替原始对象进行序列化。流中对原始对象的所有引用都将被替换为替代对象。

在替换对象时,子类必须确保替换的对象与将存储引用的每个字段兼容,或者在反序列化过程中将进行补充替换。类型不是字段或数组元素类型的子类的对象将在稍后通过引发ClassCastException中止反序列化,并且引用将不会被存储。

enableReplaceObject方法可以由ObjectOutputStream的受信任子类调用,以在序列化过程中启用一个对象替换另一个对象。替换对象在调用enableReplaceObject并传入true值之前是禁用的。之后可以通过将其设置为false来禁用。将返回先前的设置。enableReplaceObject方法检查请求替换的流是否可信。为确保不会意外暴露对象的私有状态,只有受信任的流子类才能使用replaceObject。受信任的类是属于具有启用可序列化替换权限的安全保护域的类。

如果ObjectOutputStream的子类不被视为系统域的一部分,则必须将SerializablePermission "enableSubstitution"添加到安全策略文件中。如果ObjectInputStream的子类的保护域没有通过调用enableReplaceObject获得"enableSubstitution"权限,则将抛出AccessControlException。有关安全模型的更多信息,请参阅文档Java安全体系结构(JDK1.2)。

writeStreamHeader方法将魔数和版本写入流中。这些信息必须由ObjectInputStreamreadStreamHeader方法读取。子类可能需要实现此方法以识别流的唯一格式。

flush方法用于清空流中保存的任何缓冲区,并将刷新转发给底层流。子类可以使用drain方法仅清空ObjectOutputStream的缓冲区,而无需强制刷新底层流。

所有基本类型的写入方法都使用DataOutputStream对其值进行编码,以将其放入标准流格式中。字节被缓冲到块数据记录中,以便可以将其与对象的编码区分开。此缓冲允许必要时跳过原始数据以进行类版本控制。它还允许解析流而不调用特定于类的方法。

要覆盖序列化的实现,ObjectOutputStream的子类应调用受保护的无参数ObjectOutputStream构造函数。在无参数构造函数中有一个SerializablePermission "enableSubclassImplementation"的安全检查,以确保只有受信任的类允许覆盖默认实现。此构造函数不为ObjectOutputStream分配任何私有数据,并设置一个标志,指示最终的writeObject方法应调用writeObjectOverride方法并返回。所有其他ObjectOutputStream方法都不是final的,可以直接被子类覆盖。

2.2 ObjectOutputStream.PutField类

PutField提供了为一个类的可序列化字段设置值的API,当该类不使用默认序列化时。每个方法将指定的命名值放入流中。如果name与正在写入字段的类的可序列化字段的名称不匹配,或者命名字段的类型与调用的特定put方法的第二个参数类型不匹配,则会抛出IllegalArgumentException

2.3 writeObject方法

对于可序列化对象,writeObject方法允许一个类控制其自身字段的序列化。以下是其签名:

private void writeObject(ObjectOutputStream stream)
    throws IOException;

可序列化对象的每个子类都可以定义自己的writeObject方法。如果一个类没有实现该方法,则将使用defaultWriteObject提供的默认序列化。当实现时,该类只负责写入自己的字段,而不是其超类型或子类型的字段。

如果实现了类的writeObject方法,则负责保存类的状态。在写入任何将由相应的readObject方法需要的可选数据之前,必须调用一次(仅一次)ObjectOutputStreamdefaultWriteObjectwriteFields方法;即使没有写入任何可选数据,也必须调用一次defaultWriteObjectwriteFields。如果在写入可选数据(如果有)之前未调用defaultWriteObjectwriteFields,则在ObjectInputStream无法解析定义了相关writeObject方法的类的情况下,实例反序列化的行为是未定义的。

可选数据的格式、结构和版本控制完全由类负责。

2.4 writeExternal方法

实现java.io.Externalizable接口的对象必须实现writeExternal方法来保存对象的整个状态。它必须与其超类协调以保存它们的状态。所有ObjectOutput的方法都可用于保存对象的基本类型字段和对象字段。

public void writeExternal(ObjectOutput stream)
    throws IOException;

JDK 1.2引入了一种新的写入Externalizable数据的默认格式。新格式指定原始数据将由writeExternal方法以块数据模式写入。此外,在writeExternal方法返回后,流中会附加一个标记,表示External对象的结束。这种格式变更的好处在第3.6节,“readExternal方法”中讨论。此更改引起的兼容性问题在第2.6节,“useProtocolVersion方法”中讨论。

2.5 writeReplace方法

对于Serializable和Externalizable类,writeReplace方法允许对象的类在对象被写入流之前指定其自己的替代项。通过实现writeReplace方法,一个类可以直接控制其自身实例的类型和实例被序列化时的情况。

该方法定义如下:

ANY-ACCESS-MODIFIER Object writeReplace()
             throws ObjectStreamException;

ObjectOutputStream准备将对象写入流时,将调用writeReplace方法。ObjectOutputStream检查类是否定义了writeReplace方法。如果定义了该方法,则调用writeReplace方法,允许对象在流中指定其替代项。返回的对象应该与传入的对象类型相同,或者当读取和解析时将导致与对象所有引用兼容的类型的对象。如果不是,则在发现类型不匹配时将发生ClassCastException

2.6 useProtocolVersion方法

由于流协议的更改不向后兼容,已添加了一种机制,使当前虚拟机能够编写可被先前版本读取的序列化流。当使用向后兼容协议时,当然,使用新流格式纠正的问题将存在。

流协议版本在第6.3节,“流协议版本”中讨论。