Java Object Serialization Specification: 3 - Object Input Classes


3.1 ObjectInputStream类

ObjectInputStream实现对象反序列化。它维护流的状态,包括已经反序列化的对象集合。它的方法允许从由ObjectOutputStream写入的流中读取基本类型和对象。它管理从流中恢复对象及其引用的对象。

package java.io;

public class ObjectInputStream
    extends InputStream
    implements ObjectInput, ObjectStreamConstants
{
    public ObjectInputStream(InputStream in)
        throws StreamCorruptedException, IOException;

    public final Object readObject()
        throws OptionalDataException, ClassNotFoundException,
            IOException;

    public Object readUnshared()
        throws OptionalDataException, ClassNotFoundException,
            IOException;

    public void defaultReadObject()
        throws IOException, ClassNotFoundException,
            NotActiveException;

    public GetField readFields()
        throws IOException;

    public void registerValidation(
        ObjectInputValidation obj, int prio)
        throws NotActiveException, InvalidObjectException;

    protected ObjectStreamClass readClassDescriptor()
        throws IOException, ClassNotFoundException;

    protected Class<?> resolveClass(ObjectStreamClass v)
        throws IOException, ClassNotFoundException;

    protected Class<?> resolveProxyClass(String[] interfaces)
            throws IOException, ClassNotFoundException;

    protected Object resolveObject(Object obj)
        throws IOException;

    protected boolean enableResolveObject(boolean enable)
        throws SecurityException;

    protected void readStreamHeader()
        throws IOException, StreamCorruptedException;

    public int read() throws IOException;

    public int read(byte[] data, int offset, int length)
        throws IOException

    public int available() throws IOException;

    public void close() throws IOException;

    public boolean readBoolean() throws IOException;

    public byte readByte() throws IOException;

    public int readUnsignedByte() throws IOException;

    public short readShort() throws IOException;

    public int readUnsignedShort() throws IOException;

    public char readChar() throws IOException;

    public int readInt() throws IOException;

    public long readLong() throws IOException;

    public float readFloat() throws IOException;

    public double readDouble() throws IOException;

    public void readFully(byte[] data) throws IOException;

    public void readFully(byte[] data, int offset, int size)
        throws IOException;

    public int skipBytes(int len) throws IOException;

    public String readLine() throws IOException;

    public String readUTF() throws IOException;

    // 用于提供对可序列化字段的访问的类。
    public abstract static class GetField
    {
        public abstract ObjectStreamClass getObjectStreamClass();

        public abstract boolean defaulted(String name)
            throws IOException, IllegalArgumentException;

        public abstract char get(String name, char default)
            throws IOException, IllegalArgumentException;

        public abstract boolean get(String name, boolean default)
            throws IOException, IllegalArgumentException;

        public abstract byte get(String name, byte default)
            throws IOException, IllegalArgumentException;

        public abstract short get(String name, short default)
            throws IOException, IllegalArgumentException;

        public abstract int get(String name, int default)
            throws IOException, IllegalArgumentException;

        public abstract long get(String name, long default)
            throws IOException, IllegalArgumentException;

        public abstract float get(String name, float default)
            throws IOException, IllegalArgumentException;

        public abstract double get(String name, double default)
            throws IOException, IllegalArgumentException;

        public abstract Object get(String name, Object default)
            throws IOException, IllegalArgumentException;
    }

    protected ObjectInputStream()
        throws StreamCorruptedException, IOException;

    protected readObjectOverride()
        throws OptionalDataException, ClassNotFoundException,
            IOException;
}

单参数ObjectInputStream构造函数需要一个InputStream。构造函数调用readStreamHeader来读取和验证由相应的ObjectOutputStream.writeStreamHeader方法写入的头部和版本。如果安装了安全管理器,则此构造函数在直接或间接地被覆盖readFields和/或readUnshared方法的子类的构造函数调用时,会检查"enableSubclassImplementation" SerializablePermission

注意:ObjectInputStream构造函数会阻塞,直到完成读取序列化流头部。在创建相应的ObjectOutputStream之前等待ObjectInputStream被构造的代码将会发生死锁,因为ObjectInputStream构造函数将会阻塞,直到流中写入头部,而头部将不会被写入流,直到ObjectOutputStream构造函数执行。可以通过在创建ObjectInputStream之前创建ObjectOutputStream,或者以其他方式消除ObjectInputStream构造完成和ObjectOutputStream创建之间的时间依赖性来解决这个问题。

readObject方法用于从流中反序列化对象。它从流中读取以重建对象。

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

  2. 如果流中出现块数据记录,请抛出一个带有可用字节数的BlockDataException

  3. 如果流中的对象为null,则返回null。

  4. 如果流中的对象是对先前对象的引用,则返回该对象。

  5. 如果流中的对象是一个Class,则读取其ObjectStreamClass描述符,将其及其句柄添加到已知对象集合中,并返回相应的Class对象。

  6. 如果流中的对象是一个ObjectStreamClass,则根据第4.3节“序列化形式”中描述的格式读取其数据。将其及其句柄添加到已知对象集合中。在Java 2 SDK标准版1.3及更高版本中,如果ObjectStreamClass表示的是一个不是动态代理类的类,则调用readClassDescriptor方法来读取ObjectStreamClass。如果类描述符表示的是动态代理类,则在流上调用resolveProxyClass方法以获取描述符的本地类;否则,在流上调用resolveClass方法以获取本地类。如果无法解析类,则抛出ClassNotFoundException。返回生成的ObjectStreamClass对象。

  7. 如果流中的对象是一个String,则读取其长度信息,然后读取以修改后的UTF-8编码的字符串内容。有关详细信息,请参考第6.2节“流元素”。将String及其句柄添加到已知对象集合中,并继续到第13步。

  8. 如果流中的对象是一个数组,则读取其ObjectStreamClass和数组的长度。分配数组,并将其及其句柄添加到已知对象集合中。使用适当的方法读取每个元素的类型,并将其分配给数组。继续到第13步。

  9. 如果流中的对象是一个枚举常量,则读取其ObjectStreamClass和枚举常量名称。如果ObjectStreamClass表示的是不是枚举类型的类,则抛出InvalidClassException。通过调用java.lang.Enum.valueOf方法获取枚举常量的引用,传递绑定到接收到的ObjectStreamClass的枚举类型以及接收到的名称作为参数。如果valueOf方法抛出IllegalArgumentException,则抛出一个带有IllegalArgumentException作为原因的InvalidObjectException。将枚举常量及其句柄添加到已知对象集合中,并继续到第13步。

  10. 对于所有其他对象,从流中读取对象的ObjectStreamClass。检索该ObjectStreamClass的本地类。该类必须是可序列化或可外部化的,且不能是枚举类型。如果该类不满足这些条件,则抛出InvalidClassException

  11. 如果类是记录类。将一个初始值为null的句柄添加到已知对象集合中。

    记录对象将通过调用其规范构造函数来构造。记录类R规范构造函数是通过首先从R::getRecordComponents返回的记录R的组件的数量、顺序和声明类型构建方法描述符,然后定位与描述符匹配的R的声明构造函数来找到的。如果找不到规范构造函数,则抛出InvalidClassException

    内容恢复如下:

    1. 从流中读取并恢复所有字段值。将流字段与用于初始化记录组件的适当构造函数参数进行匹配。匹配基于流字段的名称与记录组件的名称的相等性,它们的名称必须相同。也就是说,对于每个由Class::getRecordComponents返回的记录组件,首先确定组件的名称n;然后找到其名称等于n的流字段的流值。匹配字段的流值的具体类型必须可以赋值(Class::isAssignableFrom)给其匹配的记录组件的类型,否则将抛出ClassCastException -- 所有匹配都经过类型检查。未匹配的流字段将被有效地丢弃。

    2. 使用匹配的流字段值调用记录的规范构造函数。将流字段值传递给构造函数参数的相应记录组件位置。未匹配的组件将传递适合其类型的默认值。如果构造函数调用引发异常,则抛出一个带有该异常作为原因的InvalidObjectException。否则,新创建的记录对象将分配给其已知对象的句柄。继续到第13步。

  12. 分配该类的一个实例。将该实例及其句柄添加到已知对象集合中。适当地恢复内容:

    1. 对于可序列化对象,运行第一个非可序列化超类型的无参构造函数。对于可序列化类,将字段初始化为适合其类型的默认值。然后通过调用特定于类的readObject方法或如果未定义这些方法,则通过调用defaultReadObject方法来恢复每个类的字段。请注意,在反序列化期间,不会执行字段初始化程序和构造函数。在正常情况下,写入流的类的版本将与读取流的类相同。在这种情况下,流中对象的所有超类型将与当前加载的类中的超类型匹配。如果写入流的类的版本与加载的类的超类型不同,则ObjectInputStream必须更加小心地恢复或初始化不同类的状态。它必须逐个类地遍历,将流中的可用数据与正在恢复的对象的类进行匹配。出现在流中但不出现在对象中的类的数据将被丢弃。对于出现在对象中但不出现在流中的类,类字段将默认设置为默认值。

    2. 对于可外部化对象,运行该类的无参构造函数,然后调用readExternal方法来恢复对象的内容。

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

    1. 如果对象的类不是枚举类型并定义了适当的readResolve方法,则调用该方法以允许对象替换自身。

    2. 然后,如果之前通过enableResolveObject启用了,将调用resolveObject方法以允许流的子类检查和替换对象。如果前一步替换了原始对象,则将使用替换对象调用resolveObject方法。如果发生替换,则已知对象表将更新,以便将替换对象与句柄关联。然后从readObject返回替换对象。

读取基本类型的所有方法仅从流中的块数据记录中消耗字节。如果在下一个项目为对象的情况下发生对基本数据的读取,则读取方法将适当地返回-1或适当的EOFException。基本类型的值由DataInputStream从块数据记录中读取。

抛出的异常反映了遍历过程中的错误或底层流中发生的异常。如果抛出任何异常,则底层流将处于未知且不可用的状态。

当流中出现重置标记时,将丢弃流的所有状态。已知对象集合将被清除。

当流中出现异常标记时,将读取异常并抛出一个带有终止异常作为参数的新WriteAbortedException。流上下文将如前所述重置。

使用readUnshared方法从流中读取“非共享”对象。此方法与readObject相同,只是它阻止后续调用readObjectreadUnshared返回原始调用readUnshared返回的反序列化实例的其他引用。具体地:

通过readUnshared反序列化对象会使与返回对象关联的流句柄无效。请注意,这本身并不总是保证readUnshared返回的引用是唯一的;反序列化对象可能定义一个readResolve方法,该方法返回对其他方可见的对象,或者readUnshared可能返回一个可以通过流中的其他位置或外部手段获得的Class对象或枚举常量。如果反序列化对象定义了一个readResolve方法,并且该方法的调用返回一个数组,则readUnshared将返回该数组的浅克隆;这确保了返回的数组对象是唯一的,并且即使在底层数据流已被操纵的情况下,也不能通过对ObjectInputStream上的readObjectreadUnshared的第二次调用获得。

defaultReadObject方法用于从流中读取字段和对象。它使用流中的类描述符按名称和类型的规范顺序读取字段。这些值按名称在当前类中匹配的字段被赋值。有关版本控制机制的详细信息,请参阅第5.5节“兼容的Java类型演进”。对象的任何字段如果不在流中出现,则将设置为其默认值。出现在流中但不在对象中的值将被丢弃。这主要发生在类的后续版本写入了不在早期版本中出现的附加字段的情况下。此方法只能在恢复类的字段时从readObject方法中调用。在任何其他时间调用它时,将抛出NotActiveException

readFields方法从流中读取可序列化字段的值,并通过GetField类使其可用。 readFields方法只能从可序列化类的readObject方法内调用。 它不能被多次调用,也不能在调用defaultReadObject后调用。 GetFields对象使用当前对象的ObjectStreamClass来验证可以为该类检索的字段。 readFields返回的GetFields对象仅在调用类的readObject方法期间有效。 字段可以以任何顺序检索。 只有在调用类的readFields方法后才能直接从流中读取附加数据。

registerValidation方法可用于在整个图形已恢复但在将对象返回给readObject的原始调用者之前请求回调。 可以使用优先级控制验证回调的顺序。 使用较高值注册的回调在使用较低值注册的回调之前调用。 要验证的对象必须支持ObjectInputValidation接口并实现validateObject方法。 仅在调用类的readObject方法期间注册验证才是正确的。 否则,将抛出NotActiveException。 如果提供给registerValidation的回调对象为null,则会抛出InvalidObjectException

从Java SDK,标准版,v1.3开始,readClassDescriptor方法用于读取所有ObjectStreamClass对象。 当ObjectInputStream期望类描述符作为序列化流中的下一个项目时,将调用readClassDescriptorObjectInputStream的子类可以覆盖此方法以读取以非标准格式编写的类描述符(由覆盖writeClassDescriptor方法的ObjectOutputStream的子类编写)。 默认情况下,此方法根据第6.4节,“流格式语法”中描述的格式读取类描述符。

在反序列化类时,resolveClass方法在读取类描述符后调用。 子类可以扩展此方法以读取由相应的ObjectOutputStream子类编写的有关类的其他信息。 该方法必须查找并返回具有给定名称和serialVersionUID的类。 默认实现通过调用具有类加载器的最接近的调用者的类加载器来定位类。 如果找不到类,则应抛出ClassNotFoundException。 在JDK 1.1.6之前,要求resolveClass方法返回与流中的类名相同的完全限定类名。 为了适应跨版本的包重命名,resolveClass方法在JDK 1.1.6及更高版本中只需要返回具有相同基类名称和SerialVersionUID的类。

resolveObject方法由受信任的子类用于在反序列化期间监视或替换一个对象以替换另一个对象。 必须在为要解析的第一个对象调用readObject之前显式调用enableResolveObject以启用解析对象。 一旦启用,resolveObject将在每个可序列化对象之前调用一次,就在第一次从readObject返回它之前。 请注意,resolveObject方法不适用于特殊处理类的对象,如ClassObjectStreamClassString和数组。 子类的resolveObject实现可以返回替代对象,该对象将被分配或返回而不是原始对象。 返回的对象必须是与原始对象的每个引用一致且可分配的类型,否则将抛出ClassCastException。 所有赋值都经过类型检查。 流中对原始对象的所有引用将被替换为对替代对象的引用。

enableResolveObject方法由ObjectOutputStream的受信任子类调用,以在反序列化期间启用监视或替换一个对象以替换另一个对象。 替换对象直到使用true值调用enableResolveObject才被禁用。 然后可以通过将其设置为false来禁用它。 将返回先前的设置。 enableResolveObject方法检查流是否有权限在序列化期间请求替换。 为了确保不会意外公开对象的私有状态,只有受信任的流才能使用resolveObject。 受信任的类是那些类,其类加载器等于null或属于提供启用替换权限的安全保护域的类。

如果ObjectInputStream的子类不被视为系统域的一部分,则必须向安全策略文件添加一行,以便为ObjectInputStream的子类提供调用enableResolveObject的权限。 要添加的SerializablePermission"enableSubstitution"。 如果ObjectStreamClass的子类的保护域没有权限通过调用enableResolveObject"enableSubstitution",则将抛出AccessControlException。 有关安全模型的其他信息,请参阅文档Java Security Architecture(JDK 1.2)。

readStreamHeader方法读取并验证流的魔数和版本。 如果它们不匹配,则会抛出StreamCorruptedMismatch

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

3.2 ObjectInputStream.GetField类

ObjectInputStream.GetField类提供了用于获取可序列化字段值的API。 流的协议与defaultReadObject使用的相同。 使用readFields访问可序列化字段不会更改流的格式。 它只提供了一个替代API来访问值,而不需要类具有相应的非瞬态和非静态字段以用于每个命名的可序列化字段。 可序列化字段是使用serialPersistentFields声明的字段,或者如果未声明,则为对象的非瞬态和非静态字段。 当读取流时,可用的可序列化字段是在序列化对象时写入流的字段。 如果写入流的类是不同版本,则不是所有字段都将对应于当前类的可序列化字段。 可从GetField对象的ObjectStreamClass中检索可用字段。

getObjectStreamClass方法返回表示流中类的ObjectStreamClass对象。 它包含可序列化字段的列表。

defaulted方法如果字段不存在于流中,则返回true。 如果请求的字段不是当前类的可序列化字段,则会抛出IllegalArgumentException

每个get方法从流中返回指定的可序列化字段。 如果底层流引发异常,则将抛出I/O异常。 如果名称或类型与当前类的字段可序列化字段的名称和类型不匹配,则会抛出IllegalArgumentException。 如果流不包含字段的显式值,则返回默认值。

3.3 ObjectInputValidation接口

此接口允许在反序列化完整对象图后调用对象。 如果对象无法变为有效,则应抛出ObjectInvalidException。 在调用validateObject期间发生的任何异常都将终止验证过程,并将抛出InvalidObjectException

package java.io;

public interface ObjectInputValidation
{
    public void validateObject()
        throws InvalidObjectException;
}

3.4 readObject方法

对于可序列化对象,readObject方法允许类控制其自身字段的反序列化。 其签名如下:

private void readObject(ObjectInputStream stream)
    throws IOException, ClassNotFoundException;

可序列化对象的每个子类都可以定义自己的readObject方法。 如果类未实现该方法,则将使用defaultReadObject提供的默认序列化。 当实现时,类仅负责恢复其自己的字段,而不是其超类型或子类型的字段。

如果实现了类的readObject方法,则该方法负责恢复类的状态。 对象的每个字段的值,无论是瞬态还是静态,都设置为字段类型的默认值。 必须在读取由相应的writeObject方法写入的任何可选数据之前(且仅调用一次)调用ObjectInputStreamdefaultReadObjectreadFields方法; 即使不读取任何可选数据,也必须仍然调用defaultReadObjectreadFields一次。 如果类的readObject方法尝试读取比此类的流的可选部分中存在的数据更多的数据,则对于按字节读取,流将返回-1,对于原始数据读取(例如,readIntreadFloat),将抛出EOFException,或者对于对象读取,将抛出OptionalDataException,其中eof字段设置为true

可选数据的格式,结构和版本控制完全由类负责。 应该使用@serialData javadoc标签在readObject方法的javadoc注释中记录可选数据的格式和结构。

如果正在恢复的类不在正在读取的流中,则将调用其readObjectNoData方法(如果定义了该方法)(而不是readObject); 否则,其字段将初始化为适当的默认值。 有关详细信息,请参见第3.5节,“readObjectNoData方法”

ObjectInputStream中读取对象类似于创建一个新对象。就像新对象的构造函数按照从超类到子类的顺序被调用一样,从流中读取的对象是按照从超类到子类的顺序进行反序列化的。在反序列化过程中,对于每个Serializable子类,会调用readObjectreadObjectNoData方法,而不是调用构造函数。

构造函数和readObject方法之间的最后一个相似之处是,两者都提供了在对象尚未完全构建时调用对象上的方法的机会。在对象构建过程中调用的任何可重写的(既不是私有的、静态的也不是最终的)方法可能会被子类重写。在对象构建阶段调用的方法由对象的实际类型解析,而不是由当前正在由其构造函数或readObject/readObjectNoData方法初始化的类型解析。因此,在readObjectreadObjectNoData方法中调用可重写方法可能会导致在超类完全初始化之前意外调用子类方法。

3.5 readObjectNoData方法

对于可序列化对象,readObjectNoData方法允许类控制在反序列化子类实例时初始化自己字段的过程,即使序列化流中没有将该类列为反序列化对象的超类。这可能发生在接收方使用的反序列化实例类版本与发送方不同,并且接收方的版本扩展了发送方版本未扩展的类的情况下。这也可能发生在序列化流被篡改的情况下;因此,readObjectNoData对于正确初始化反序列化对象非常有用,尽管存在“敌对”或不完整的源流。

private void readObjectNoData() throws ObjectStreamException;

每个可序列化类可以定义自己的readObjectNoData方法。如果可序列化类没有定义readObjectNoData方法,那么在上述情况下,该类的字段将被初始化为它们的默认值(如Java语言规范中所列);这种行为与Java 2 SDK标准版1.4版本之前的ObjectInputStream的行为一致,当时引入了对readObjectNoData方法的支持。如果可序列化类定义了readObjectNoData方法,并且出现了上述条件,则在反序列化过程中将在本应调用类定义的readObject方法的时刻调用readObjectNoData,前提是如果该类被流列为正在反序列化的实例的超类。

3.6 readExternal方法

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

public void readExternal(ObjectInput stream)
    throws IOException;

注意:readExternal方法是公共的,它提高了客户端能够从流中覆盖现有对象的风险。类可以添加自己的检查以确保只在适当时调用该方法。

JDK 1.2引入了一个新的流协议版本,以纠正Externalizable对象的问题。旧版Externalizable对象的定义要求本地虚拟机找到readExternal方法才能正确地从流中读取Externalizable对象。新格式向流协议添加了足够的信息,以便在本地类没有可用的readExternal方法时,序列化可以跳过Externalizable对象。由于类演变规则,如果在输入流中没有为本地类映射对象,则序列化必须能够跳过Externalizable对象。

新的Externalizable流格式的另一个好处是,ObjectInputStream可以检测尝试读取超出可用数据的外部数据,并且还可以跳过readExternal方法未消耗的任何数据。当尝试读取超出外部数据末尾时,ObjectInputStream的行为与类定义的readObject方法尝试读取可选数据末尾时的行为相同:按字节读取将返回-1,原始读取将抛出EOFException,对象读取将抛出OptionalDataException,其中eof字段设置为true

由于格式更改,JDK 1.1.6及更早版本无法读取新格式。当JDK 1.1.6或更早版本尝试从使用PROTOCOL_VERSION_2编写的流中读取Externalizable对象时,将抛出StreamCorruptedException。兼容性问题在第6.3节“流协议版本”中有更详细的讨论。

3.7 readResolve方法

对于可序列化和Externalizable类,readResolve方法允许类在将对象从流中读取并返回给调用者之前替换/解析该对象。通过实现readResolve方法,类可以直接控制其实例在反序列化时的类型和实例。该方法定义如下:

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

ObjectInputStream从流中读取对象并准备将其返回给调用者时,将调用readResolve方法。ObjectInputStream检查对象的类是否定义了readResolve方法。如果定义了该方法,则将调用readResolve方法,以允许流中的对象指定要返回的对象。返回的对象应该是与所有用途兼容的类型。如果不兼容,当发现类型不匹配时将抛出ClassCastException

例如,可以创建一个Symbol类,其中每个符号绑定只存在一个实例在虚拟机中。readResolve方法将被实现以确定该符号是否已经定义,并替换预先存在的等效Symbol对象以维护标识约束。通过这种方式,可以在序列化过程中保持Symbol对象的唯一性。

注意:readResolve方法在对象完全构建之前不会对对象进行调用,因此在对象图中对该对象的任何引用将不会更新为readResolve指定的新对象。然而,在具有writeReplace方法的对象序列化期间,替换对象的对象图中对原始对象的所有引用都将替换为对替换对象的引用。因此,在序列化一个对象并指定一个对象图中的替换对象的情况下,反序列化将导致对象图的不正确。此外,如果正在读取的对象(由writeReplace指定)和原始对象的引用类型不兼容,则对象图的构建将引发ClassCastException