Record Serialization (Preview)

Changes to the Java® Object Serialization Specification Specification • Version 14.0.2+12-46

This document describes changes to the Java Object Serialization Specification to support serializable records. See JEP draft 8222777.

Changes are described with respect to existing sections of the JLS. New text is indicated like this and deleted text is indicated like this. Explanation and discussion, as needed, is set aside in grey boxes.


1 - System Architecture

1.1 Overview

The ability to store and retrieve JavaTM objects is essential to building all but the most transient applications. The key to storing and retrieving objects in a serialized form is representing the state of objects sufficient to reconstruct the object(s). Objects to be saved in the stream may support either the Serializable or the Externalizable interface. For JavaTM objects, the serialized form must be able to identify and verify the JavaTM class from which the contents of the object were saved and to restore the contents to a new instance. For serializable objects, the stream includes sufficient information to restore the fields in the stream to a compatible version of the class. For Externalizable objects, the class is solely responsible for the external format of its contents.

Objects to be stored and retrieved frequently refer to other objects. Those other objects must be stored and retrieved at the same time to maintain the relationships between the objects. When an object is stored, all of the objects that are reachable from that object are stored as well.

The goals for serializing JavaTM objects are to:

1.2 Writing to an Object Stream

Minor rewording in this subsection

Writing objects and primitives to a stream is a straightforward process. For example:

// Serialize today's date to a file.
    FileOutputStream f = new FileOutputStream("tmp");
    ObjectOutput s = new ObjectOutputStream(f);
    s.writeObject("Today");
    s.writeObject(new Date());
    s.flush();

First an OutputStream, in this case a FileOutputStream, is needed to receive the bytes. Then an ObjectOutputStream is created that writes to the FileOutputStream. Next, the string "Today" and a Date object are written to the stream. More generally, objects are written with the writeObject method and primitives are written to the stream with the methods of DataOutput.

The writeObject method (see Section 2.3, "The writeObject Method") serializes the specified object and traverses its references to other objects in the object graph recursively to create a complete serialized representation of the graph. Within a stream, the first reference to any object results in the object being serialized or externalized and the assignment of a handle for that object. Subsequent references to that object are encoded as the handle. Using object handles preserves sharing and circular references that occur naturally in object graphs. Subsequent references to an object use only the handle allowing a very compact representation. The use of handles preserves the sharing of objects that occurs naturally in object graphs, and allows for circular references between objects (that is, cycles in the graph).

Special handling is required for arrays, enum constants, and objects of type Class, ObjectStreamClass, and String. Other objects must implement either the Serializable or the Externalizable interface to be saved in or restored from a stream.

Primitive data types are written to the stream with the methods in the DataOutput interface, such as writeInt, writeFloat, or writeUTF. Individual bytes and arrays of bytes are written with the methods of OutputStream. Except for serializable fields, primitive data is written to the stream in block-data records, with each record prefixed by a marker and an indication of the number of bytes in the record.

ObjectOutputStream can be extended to customize the information about classes in the stream or to replace objects to be serialized. Refer to the annotateClass and replaceObject method descriptions for details.

1.3 Reading from an Object Stream

Reading an object from a stream, like writing, is straightforward:

// Deserialize a string and date from a file.
    FileInputStream in = new FileInputStream("tmp");
    ObjectInputStream s = new ObjectInputStream(in);
    String today = (String)s.readObject();
    Date date = (Date)s.readObject();

First an InputStream, in this case a FileInputStream, is needed as the source stream. Then an ObjectInputStream is created that reads from the InputStream. Next, the string "Today" and a Date object are read from the stream. Generally, objects are read with the readObject method and primitives are read from the stream with the methods of DataInput.

The readObject method deserializes the next object in the stream and traverses its references to other objects recursively to create the complete graph of objects serialized.

Primitive data types are read from the stream with the methods in the DataInput interface, such as readInt, readFloat, or readUTF. Individual bytes and arrays of bytes are read with the methods of InputStream. Except for serializable fields, primitive data is read from block-data records.

ObjectInputStream can be extended to utilize customized information in the stream about classes or to replace objects that have been deserialized. Refer to the resolveClass and resolveObject method descriptions for details.

1.4 Object Streams as Containers

Object Serialization produces and consumes a stream of bytes that contain one or more primitives and objects. The objects written to the stream, in turn, refer to other objects, which are also represented in the stream. Object Serialization produces just one stream format that encodes and stores the contained objects.

Each object that acts as a container implements an interface which allows primitives and objects to be stored in or retrieved from it. These interfaces are the ObjectOutput and ObjectInput interfaces which:

Each object which is to be stored in a stream must explicitly allow itself to be stored and must implement the protocols needed to save and restore its state. Object Serialization defines two such protocols. The protocols allow the container to ask the object to write and read its state.

To be stored in an Object Stream, each object must implement either the Serializable or the Externalizable interface:

1.5 Defining Serializable Fields for a Class

The serializable fields of a class can be defined two different ways. Default serializable fields of a class are defined to be the non-transient and non-static fields. This default computation can be overridden by declaring a special field in the Serializable class, serialPersistentFields. This field must be initialized with an array of ObjectStreamField objects that list the names and types of the serializable fields. The modifiers for the field are required to be private, static, and final. If the field's value is null or is otherwise not an instance of ObjectStreamField[], or if the field does not have the required modifiers, then the behavior is as if the field were not declared at all.

For example, the following declaration duplicates the default behavior.

class List implements Serializable {
    List next;

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

}

By using serialPersistentFields to define the Serializable fields for a class, there no longer is a limitation that a serializable field must be a field within the current definition of the Serializable class. The writeObject and readObject methods of the Serializable class can map the current implementation of the class to the serializable fields of the class using the interface that is described in Section 1.7, "Accessing Serializable Fields of a Class". Therefore, the fields for a Serializable class can change in a later release, as long as it maintains the mapping back to its Serializable fields that must remain compatible across release boundaries.

Note: There is, however, a limitation to the use of this mechanism to specify serializable fields for inner classes. Inner classes can only contain final static fields that are initialized to constants or expressions built up from constants. Consequently, it is not possible to set serialPersistentFields for an inner class (though it is possible to set it for static member classes). For other restrictions pertaining to serialization of inner class instances, see section Section 1.10, "The Serializable Interface".

1.6 Documenting Serializable Fields and Data for a Class

It is important to document the serializable state of a class to enable interoperability with alternative implementations of a Serializable class and to document class evolution. Documenting a serializable field gives one a final opportunity to review whether or not the field should be serializable. The serialization javadoc tags, @serial, @serialField, and @serialData, provide a way to document the serialized form for a Serializable class within the source code.

The javadoc application recognizes the serialization javadoc tags and generates a specification for each Serializable and Externalizable class. See Section C.1, "Example Alternate Implementation of java.io.File" for an example that uses these tags.

When a class is declared Serializable, the serializable state of the object is defined by serializable fields (by name and type) plus optional data. Optional data can only be written explicitly by the writeObject method of a Serializable class. Optional data can be read by the Serializable class' readObject method or serialization will skip unread optional data.

When a class is declared Externalizable, the data that is written to the stream by the class itself defines the serialized state. The class must specify the order, types, and meaning of each datum that is written to the stream. The class must handle its own evolution, so that it can continue to read data written by and write data that can be read by previous versions. The class must coordinate with the superclass when saving and restoring data. The location of the superclasses data in the stream must be specified.

The designer of a Serializable class must ensure that the information saved for the class is appropriate for persistence and follows the serialization-specified rules for interoperability and evolution. Class evolution is explained in greater detail in Chapter 5, "Versioning of Serializable Objects".

1.7 Accessing Serializable Fields of a Class

Serialization provides two mechanisms for accessing the serializable fields in a stream:

The default mechanism is used automatically when reading or writing objects that implement the Serializable interface and do no further customization. The serializable fields are mapped to the corresponding fields of the class and values are either written to the stream from those fields or are read in and assigned respectively. If the class provides writeObject and readObject methods, the default mechanism can be invoked by calling defaultWriteObject and defaultReadObject. When the writeObject and readObject methods are implemented, the class has an opportunity to modify the serializable field values before they are written or after they are read.

When the default mechanism cannot be used, the serializable class can use the putFields method of ObjectOutputStream to put the values for the serializable fields into the stream. The writeFields method of ObjectOutputStream puts the values in the correct order, then writes them to the stream using the existing protocol for serialization. Correspondingly, the readFields method of ObjectInputStream reads the values from the stream and makes them available to the class by name in any order. See Section 2.2, "The ObjectOutputStream.PutField Class" and Section 3.2, "The ObjectInputStream.GetField Class" for a detailed description of the Serializable Fields API.

1.8 The ObjectOutput Interface

The ObjectOutput interface provides an abstract, stream-based interface to object storage. It extends the DataOutput interface so those methods can be used for writing primitive data types. Objects that implement this interface can be used to store primitives and objects.

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;
}

The writeObject method is used to write an object. The exceptions thrown reflect errors while accessing the object or its fields, or exceptions that occur in writing to storage. If any exception is thrown, the underlying storage may be corrupted. If this occurs, refer to the object that is implementing this interface for more information.

1.9 The ObjectInput Interface

The ObjectInput interface provides an abstract stream based interface to object retrieval. It extends the DataInput interface so those methods for reading primitive data types are accessible in this interface.

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;
}

The readObject method is used to read and return an object. The exceptions thrown reflect errors while accessing the objects or its fields or exceptions that occur in reading from the storage. If any exception is thrown, the underlying storage may be corrupted. If this occurs, refer to the object implementing this interface for additional information.

1.10 The Serializable Interface

Object Serialization produces a stream with information about the JavaTM classes for the objects which are being saved. For serializable objects, sufficient information is kept to restore those objects even if a different (but compatible) version of the implementation of the class is present. The Serializable interface is defined to identify classes which implement the serializable protocol:

package java.io;

public interface Serializable {};

A Serializable class must do the following:

The class can optionally define the following methods:

ObjectOutputStream and ObjectInputStream allow the serializable classes on which they operate to evolve (allow changes to the classes that are compatible with the earlier versions of the classes). See Section 5.5, "Compatible Java Type Evolution" for information about the mechanism which is used to allow compatible changes.

Note: Serialization of inner classes (i.e., nested classes that are not static member classes), including local and anonymous classes, is strongly discouraged for several reasons. Because inner classes declared in non-static contexts contain implicit non-transient references to enclosing class instances, serializing such an inner class instance will result in serialization of its associated outer class instance as well. Synthetic fields generated by javac (or other JavaTM compilers) to implement inner classes are implementation dependent and may vary between compilers; differences in such fields can disrupt compatibility as well as result in conflicting default serialVersionUID values. The names assigned to local and anonymous inner classes are also implementation dependent and may differ between compilers. Since inner classes cannot declare static members other than compile-time constant fields, they cannot use the serialPersistentFields mechanism to designate serializable fields. Finally, because inner classes associated with outer instances do not have zero-argument constructors (constructors of such inner classes implicitly accept the enclosing instance as a prepended parameter), they cannot implement Externalizable. None of the issues listed above, however, apply to static member classes.

1.11 The Externalizable Interface

For Externalizable objects, only the identity of the class of the object is saved by the container; the class must save and restore the contents. The Externalizable interface is defined as follows:

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;
}

The class of an Externalizable object must do the following:

An Externalizable class can optionally define the following methods:

1.12 Serialization of Enum Constants

Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant's name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method, passing the constant's enum type along with the received constant name as arguments.

Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream.

The process by which enum constants are serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored--all enum types have a fixed serialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.

1.13 Serialization of Records

This is a new subsection

Records are serialized differently than ordinary serializable or externalizable objects. The serialized form of a record object is a sequence of values derived from the record components. The stream format of a record object is the same as that of an ordinary object in the stream. During deserialization, if the local class equivalent of the specified stream class descriptor is a record class, then first the stream fields are read and reconstructed to serve as the record's component values; and second, a record object is created by invoking the record's canonical constructor with the component values as arguments (or the default value for component's type if a component value is absent from the stream).

Like other serializable or externalizable objects, record objects can function as the target of back references appearing subsequently in the serialization stream. However, a cycle in the graph where the record object is referred to, either directly or transitively, by one of its components, is not preserved. The record components are deserialized prior to the invocation of the record constructor, hence this limitation (see Section 1.14, "Circular References" for additional information).

The process by which record objects are serialized or externalized cannot be customized; any class-specific writeObject, readObject, readObjectNoData, writeExternal, and readExternal methods defined by record classes are ignored during serialization and deserialization. However, a substitute object to be serialized or a designate replacement may be specified, by the writeReplace and readResolve methods, respectively. Any serialPersistentFields field declaration is ignored. Documenting serializable fields and data for record classes is unnecessary, since there is no variation in the serial form, other than whether a substitute or replacement object is used. The serialVersionUID of a record class is 0L unless explicitly declared. The requirement for matching serialVersionUID values is waived for record classes.

1.14 Circular References

This is a new subsection

As described in Section 1.2, " Writing to an Object Stream", the use of handles can preserve cyclic references that occur in object graphs.

A minimal contrived example, for illustration purposes only:

    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; }
    }

    // create an instance of both Data and Carrier, and a cycle between them
    Data d1 = new Data();
    Carrier c1 = new Carrier(d1);
    d1.obj = c1;

    // serialize
    ObjectOutputStream oos = new ObjectOutputStream(...);
    oos.writeObject(c1);

    // deserialize
    ObjectInputStream ois  = new ObjectInputStream(...);
    Carrier c2 = (Carrier) ois.readObject();

When deserialized, the object, c2, will have its d field refer to an instance of Data, that will in turn have its obj field refer back to the same instance of c2. The identity of the object referred to by c2 is equal to the identity of the object referred to by c2.d().obj, that is c2 == c2.d().obj.

The allocation of the object, c2, and the assignment of its handle, happen before the reconstruction of its field values (see Section 3.1 "The ObjectInputStream Class", step 12). This allows the field values (and their field values recursively) to refer to the handle for c2 during deserialization. In this way, deserialization of ordinary objects supports cycles in the object graph.

Now consider if Carrier were a record class, as follows:

    record Carrier(Data d) implements Serializable { }

When deserialized, the object, c2, will have its d field refer to an instance of Data, that will in turn have its obj field refer to null (rather than referring to c2). The circular reference, through d.obj, in the original object graph, is not preserved during deserialization.

The allocation of the record object c2, and the assignment of its handle, happen after the reconstruction of its field values ( that is, the component values of the future record; see Section 3.1 "The ObjectInputStream Class", step 11). While a handle for the record object is added to the set of known objects before the record component values are reconstructed, it has an initial value of null. The handle is assigned to the record object only after the record object has been constructed (through an invocation of its canonical constructor). Consequently, during deserialization of the record component values, references in the stream to the handle for the record object, will see the initial null value. As such, cycles targeting the record object from its components (or their fields transitively) are not preserved during deserialization.

1.135 Protecting Sensitive Information

When developing a class that provides controlled access to resources, care must be taken to protect sensitive information and functions. During deserialization, the private state of the object is restored. For example, a file descriptor contains a handle that provides access to an operating system resource. Being able to forge a file descriptor would allow some forms of illegal access, since restoring state is done from a stream. Therefore, the serializing runtime must take the conservative approach and not trust the stream to contain only valid representations of objects. To avoid compromising a class, the sensitive state of an object must not be restored from the stream, or it must be reverified by the class. Several techniques are available to protect sensitive data in classes.

The easiest technique is to mark fields that contain sensitive data as private transient. Transient fields are not persistent and will not be saved by any persistence mechanism. Marking the field will prevent the state from appearing in the stream and from being restored during deserialization. Since writing and reading (of private fields) cannot be superseded outside of the class, the transient fields of the class are safe.

Particularly sensitive classes should not be serialized at all. To accomplish this, the object should not implement either the Serializable or the Externalizable interface.

Some classes may find it beneficial to allow writing and reading but specifically handle and revalidate the state as it is deserialized. The class should implement writeObject and readObject methods to save and restore only the appropriate state. If access should be denied, throwing a NotSerializableException will prevent further access.


2 - Object Output Classes

2.1 The ObjectOutputStream Class

An additional step has been added

Class ObjectOutputStream implements object serialization. It maintains the state of the stream including the set of objects already serialized. Its methods control the traversal of objects to be serialized to save the specified objects and the objects to which they refer.

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 writeFields()
        throws IOException;

    public void reset() throws IOException;

    protected void annotateClass(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.
    abstract static public class PutField
    {
        public void put(String name, boolean value)
            throws IOException, IllegalArgumentException;

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

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

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

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

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

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

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

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

    public void useProtocolVersion(int version) throws IOException;

    protected ObjectOutputStream()
        throws IOException;

     protected writeObjectOverride()
        throws NotActiveException, IOException;
}

The single-argument ObjectOutputStream constructor creates an ObjectOutputStream that serializes objects to the given OutputStream. The constructor calls writeStreamHeader to write a magic number and version to the stream that will be read and verified by a corresponding call to readStreamHeader in the single-argument ObjectInputStream constructor. If a security manager is installed, this constructor checks for the "enableSubclassImplementation" SerializablePermission when invoked directly or indirectly by the constructor of a subclass which overrides the putFields and/or writeUnshared methods.

The writeObject method is used to serialize an object to the stream. An object is serialized as follows:

  1. If a subclass is overriding the implementation, call the writeObjectOverride method and return. Overriding the implementation is described at the end of this section.

  2. If there is data in the block-data buffer, the data is written to the stream and the buffer is reset.

  3. If the object is null, null is put in the stream and writeObject returns.

  4. If the object has been previously replaced, as described in Step 8, write the handle of the replacement to the stream and writeObject returns.

  5. If the object has already been written to the stream, its handle is written to the stream and writeObject returns.

  6. If the object is a Class, the corresponding ObjectStreamClass is written to the stream, a handle is assigned for the class, and writeObject returns.

  7. If the object is an ObjectStreamClass, a handle is assigned to the object, after which it is written to the stream using one of the class descriptor formats described in Section 4.3, "Serialized Form". In versions 1.3 and later of the Java 2 SDK, Standard Edition, the writeClassDescriptor method is called to output the ObjectStreamClass if it represents a class that is not a dynamic proxy class, as determined by passing the associated Class object to the isProxyClass method of java.lang.reflect.Proxy. Afterwards, an annotation for the represented class is written: if the class is a dynamic proxy class, then the annotateProxyClass method is called; otherwise, the annotateClass method is called. The writeObject method then returns.

  8. Process potential substitutions by the class of the object and/or by a subclass of ObjectInputStream.

    1. If the class of an object is not an enum type and defines the appropriate writeReplace method, the method is called. Optionally, it can return a substitute object to be serialized.

    2. Then, if enabled by calling the enableReplaceObject method, the replaceObject method is called to allow subclasses of ObjectOutputStream to substitute for the object being serialized. If the original object was replaced in the previous step, the replaceObject method is called with the replacement object.

    If the original object was replaced by either one or both steps above, the mapping from the original object to the replacement is recorded for later use in Step 4. Then, Steps 3 through 7 are repeated on the new object.

    If the replacement object is not one of the types covered by Steps 3 through 7, processing resumes using the replacement object at Step 10.

  9. If the object is a java.lang.String, the string is written as length information followed by the contents of the string encoded in modified UTF-8. For details, refer to Section 6.2, "Stream Elements". A handle is assigned to the string, and writeObject returns.

  10. If the object is an array, writeObject is called recursively to write the ObjectStreamClass of the array. The handle for the array is assigned. It is followed by the length of the array. Each element of the array is then written to the stream, after which writeObject returns.

  11. If the object is an enum constant, the ObjectStreamClass for the enum type of the constant is written by recursively calling writeObject. It will appear in the stream only the first time it is referenced. A handle is assigned for the enum constant. Next, the value returned by the name method of the enum constant is written as a String object, as described in step 9. Note that if the same name string has appeared previously in the stream, a back reference to it will be written. The writeObject method then returns.

  1. If the object is a record object, the ObjectStreamClass for the class of the record object is written by recursively calling writeObject. It will appear in the stream only the first time it is referenced. A handle is assigned for the record object.

    The components of the record object are written to the stream.

     a.  If the record object is serializable or externalizable, the record
         components are written, as if by invoking the `defaultWriteObject`
         method.
    
     b.  If the object is neither serializable or externalizable, the
         `NotSerializableException` is thrown.
    The writeObject method then returns.

123. For regular objects, the ObjectStreamClass for the class of the object is written by recursively calling writeObject. It will appear in the stream only the first time it is referenced. A handle is assigned for the object.

    The contents of the object are written to the stream.

    a.  If the object is serializable, the highest serializable class is
        located. For that class, and each derived class, that class's fields
        are written. If the class does not have a `writeObject` method, the
        `defaultWriteObject` method is called to write the serializable fields
        to the stream. If the class does have a `writeObject` method, it is
        called. It may call `defaultWriteObject` or `putFields` and
        `writeFields` to save the state of the object, and then it can write
        other information to the stream.

    b.  If the object is externalizable, the `writeExternal` method of the
        object is called.

    c.  If the object is neither serializable or externalizable, the
        `NotSerializableException` is thrown.

Exceptions may occur during the traversal or may occur in the underlying stream. For any subclass of IOException, the exception is written to the stream using the exception protocol and the stream state is discarded. If a second IOException is thrown while attempting to write the first exception into the stream, the stream is left in an unknown state and StreamCorruptedException is thrown from writeObject. For other exceptions, the stream is aborted and left in an unknown and unusable state.

The writeUnshared method writes an "unshared" object to the ObjectOutputStream. This method is identical to writeObject, except that it always writes the given object as a new, unique object in the stream (as opposed to a back-reference pointing to a previously serialized instance). Specifically:

While writing an object via writeUnshared does not in itself guarantee a unique reference to the object when it is deserialized, it allows a single object to be defined multiple times in a stream, so that multiple calls to the ObjectInputStream.readUnshared method (see Section 3.1, "The ObjectInputStream Class") by the receiver will not conflict. Note that the rules described above only apply to the base-level object written with writeUnshared, and not to any transitively referenced sub-objects in the object graph to be serialized.

The defaultWriteObject method implements the default serialization mechanism for the current class. This method may be called only from a class's writeObject method. The method writes all of the serializable fields of the current class to the stream. If called from outside the writeObject method, the NotActiveException is thrown.

The putFields method returns a PutField object the caller uses to set the values of the serializable fields in the stream. The fields may be set in any order. After all of the fields have been set, writeFields must be called to write the field values in the canonical order to the stream. If a field is not set, the default value appropriate for its type will be written to the stream. This method may only be called from within the writeObject method of a serializable class. It may not be called more than once or if defaultWriteObject has been called. Only after writeFields has been called can other data be written to the stream.

The reset method resets the stream state to be the same as if it had just been constructed. Reset will discard the state of any objects already written to the stream. The current point in the stream is marked as reset, so the corresponding ObjectInputStream will reset at the same point. Objects previously written to the stream will not be remembered as already having been written to the stream. They will be written to the stream again. This is useful when the contents of an object or objects must be sent again. Reset may not be called while objects are being serialized. If called inappropriately, an IOException is thrown.

Starting with the Java 2 SDK, Standard Edition, v1.3, the writeClassDescriptor method is called when an ObjectStreamClass needs to be serialized. writeClassDescriptor is responsible for writing a representation of the ObjectStreamClass to the serialization stream. Subclasses may override this method to customize the way in which class descriptors are written to the serialization stream. If this method is overridden, then the corresponding readClassDescriptor method in ObjectInputStream should also be overridden to reconstitute the class descriptor from its custom stream representation. By default, writeClassDescriptor writes class descriptors according to the format specified in Section 6.4, "Grammar for the Stream Format". Note that this method will only be called if the ObjectOutputStream is not using the old serialization stream format (see Section 6.3, "Stream Protocol Versions"). If the serialization stream is using the old format (ObjectStreamConstants.PROTOCOL_VERSION_1), the class descriptor will be written internally in a manner that cannot be overridden or customized.

The annotateClass method is called while a Class is being serialized, and after the class descriptor has been written to the stream. Subclasses may extend this method and write other information to the stream about the class. This information must be read by the resolveClass method in a corresponding ObjectInputStream subclass.

An ObjectOutputStream subclass can implement the replaceObject method to monitor or replace objects during serialization. Replacing objects must be enabled explicitly by calling enableReplaceObject before calling writeObject with the first object to be replaced. Once enabled, replaceObject is called for each object just prior to serializing the object for the first time. Note that the replaceObject method is not called for objects of the specially handled classes, Class and ObjectStreamClass. An implementation of a subclass may return a substitute object that will be serialized instead of the original. The substitute object must be serializable. All references in the stream to the original object will be replaced by the substitute object.

When objects are being replaced, the subclass must ensure that the substituted object is compatible with every field where the reference will be stored, or that a complementary substitution will be made during deserialization. Objects, whose type is not a subclass of the type of the field or array element, will later abort the deserialization by raising a ClassCastException and the reference will not be stored.

The enableReplaceObject method can be called by trusted subclasses of ObjectOutputStream to enable the substitution of one object for another during serialization. Replacing objects is disabled until enableReplaceObject is called with a true value. It may thereafter be disabled by setting it to false. The previous setting is returned. The enableReplaceObject method checks that the stream requesting the replacement can be trusted. To ensure that the private state of objects is not unintentionally exposed, only trusted stream subclasses may use replaceObject. Trusted classes are those classes that belong to a security protection domain with permission to enable Serializable substitution.

If the subclass of ObjectOutputStream is not considered part of the system domain, SerializablePermission "enableSubstitution" must be added to the security policy file. AccessControlException is thrown if the protection domain of the subclass of ObjectInputStream does not have permission to "enableSubstitution" by calling enableReplaceObject. See the document Java Security Architecture (JDK1.2) for additional information about the security model.

The writeStreamHeader method writes the magic number and version to the stream. This information must be read by the readStreamHeader method of ObjectInputStream. Subclasses may need to implement this method to identify the stream's unique format.

The flush method is used to empty any buffers being held by the stream and to forward the flush to the underlying stream. The drain method may be used by subclassers to empty only the ObjectOutputStream's buffers without forcing the underlying stream to be flushed.

All of the write methods for primitive types encode their values using a DataOutputStream to put them in the standard stream format. The bytes are buffered into block data records so they can be distinguished from the encoding of objects. This buffering allows primitive data to be skipped if necessary for class versioning. It also allows the stream to be parsed without invoking class-specific methods.

To override the implementation of serialization, the subclass of ObjectOutputStream should call the protected no-arg ObjectOutputStream, constructor. There is a security check within the no-arg constructor for SerializablePermission "enableSubclassImplementation" to ensure that only trusted classes are allowed to override the default implementation. This constructor does not allocate any private data for ObjectOutputStream and sets a flag that indicates that the final writeObject method should invoke the writeObjectOverride method and return. All other ObjectOutputStream methods are not final and can be directly overridden by the subclass.

2.2 The ObjectOutputStream.PutField Class

Class PutField provides the API for setting values of the serializable fields for a class when the class does not use default serialization. Each method puts the specified named value into the stream. An IllegalArgumentException is thrown if name does not match the name of a serializable field for the class whose fields are being written, or if the type of the named field does not match the second parameter type of the specific put method invoked.

2.3 The writeObject Method

For serializable objects, the writeObject method allows a class to control the serialization of its own fields. Here is its signature:

private void writeObject(ObjectOutputStream stream)
    throws IOException;

Each subclass of a serializable object may define its own writeObject method. If a class does not implement the method, the default serialization provided by defaultWriteObject will be used. When implemented, the class is only responsible for writing its own fields, not those of its supertypes or subtypes.

The class's writeObject method, if implemented, is responsible for saving the state of the class. Either ObjectOutputStream's defaultWriteObject or writeFields method must be called once (and only once) before writing any optional data that will be needed by the corresponding readObject method to restore the state of the object; even if no optional data is written, defaultWriteObject or writeFields must still be invoked once. If defaultWriteObject or writeFields is not invoked once prior to the writing of optional data (if any), then the behavior of instance deserialization is undefined in cases where the ObjectInputStream cannot resolve the class which defined the writeObject method in question.

The responsibility for the format, structure, and versioning of the optional data lies completely with the class.

2.4 The writeExternal Method

Objects implementing java.io.Externalizable must implement the writeExternal method to save the entire state of the object. It must coordinate with its superclasses to save their state. All of the methods of ObjectOutput are available to save the object's primitive typed fields and object fields.

public void writeExternal(ObjectOutput stream)
    throws IOException;

A new default format for writing Externalizable data has been introduced in JDK 1.2. The new format specifies that primitive data will be written in block data mode by writeExternal methods. Additionally, a tag denoting the end of the External object is appended to the stream after the writeExternal method returns. The benefits of this format change are discussed in Section 3.6, "The readExternal Method". Compatibility issues caused by this change are discussed in Section 2.6, "The useProtocolVersion Method".

2.5 The writeReplace Method

For Serializable and Externalizable classes, the writeReplace method allows a class of an object to nominate its own replacement in the stream before the object is written. By implementing the writeReplace method, a class can directly control the types and instances of its own instances being serialized.

The method is defined as follows:

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

The writeReplace method is called when ObjectOutputStream is preparing to write the object to the stream. The ObjectOutputStream checks whether the class defines the writeReplace method. If the method is defined, the writeReplace method is called to allow the object to designate its replacement in the stream. The object returned should be either of the same type as the object passed in or an object that when read and resolved will result in an object of a type that is compatible with all references to the object. If it is not, a ClassCastException will occur when the type mismatch is discovered.

2.6 The useProtocolVersion Method

Due to a stream protocol change that was not backwards compatible, a mechanism has been added to enable the current Virtual Machine to write a serialization stream that is readable by a previous release. Of course, the problems that are corrected by the new stream format will exist when using the backwards compatible protocol.

Stream protocol versions are discussed in Section 6.3, "Stream Protocol Versions".


3 - Object Input Classes

3.1 The ObjectInputStream Class

An additional step has been added

Class ObjectInputStream implements object deserialization. It maintains the state of the stream including the set of objects already deserialized. Its methods allow primitive types and objects to be read from a stream written by ObjectOutputStream. It manages restoration of the object and the objects that it refers to from the stream.

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 synchronized void registerValidation(
        ObjectInputValidation obj, int prio)
        throws NotActiveException, InvalidObjectException;

    protected ObjectStreamClass readClassDescriptor()
        throws IOException, ClassNotFoundException;

    protected Class resolveClass(ObjectStreamClass v)
        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;

    // Class to provide access to serializable fields.
    static abstract public class GetField
    {
        public ObjectStreamClass getObjectStreamClass();

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

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

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

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

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

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

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

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

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

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

    protected ObjectInputStream()
        throws StreamCorruptedException, IOException;

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

The single-argument ObjectInputStream constructor requires an InputStream. The constructor calls readStreamHeader to read and verifies the header and version written by the corresponding ObjectOutputStream.writeStreamHeader method. If a security manager is installed, this constructor checks for the "enableSubclassImplementation" SerializablePermission when invoked directly or indirectly by the constructor of a subclass which overrides the readFields and/or readUnshared methods.

Note: The ObjectInputStream constructor blocks until it completes reading the serialization stream header. Code which waits for an ObjectInputStream to be constructed before creating the corresponding ObjectOutputStream for that stream will deadlock, since the ObjectInputStream constructor will block until a header is written to the stream, and the header will not be written to the stream until the ObjectOutputStream constructor executes. This problem can be resolved by creating the ObjectOutputStream before the ObjectInputStream, or otherwise removing the timing dependency between completion of ObjectInputStream construction and the creation of the ObjectOutputStream.

The readObject method is used to deserialize an object from the stream. It reads from the stream to reconstruct an object.

  1. If the ObjectInputStream subclass is overriding the implementation, call the readObjectOverride method and return. Reimplementation is described at the end of this section.

  2. If a block data record occurs in the stream, throw a BlockDataException with the number of available bytes.

  3. If the object in the stream is null, return null.

  4. If the object in the stream is a handle to a previous object, return the object.

  5. If the object in the stream is a Class, read its ObjectStreamClass descriptor, add it and its handle to the set of known objects, and return the corresponding Class object.

  6. If the object in the stream is an ObjectStreamClass, read in its data according to the formats described in Section 4.3, "Serialized Form". Add it and its handle to the set of known objects. In versions 1.3 and later of the Java 2 SDK, Standard Edition, the readClassDescriptor method is called to read in the ObjectStreamClass if it represents a class that is not a dynamic proxy class, as indicated in the stream data. If the class descriptor represents a dynamic proxy class, call the resolveProxyClass method on the stream to get the local class for the descriptor; otherwise, call the resolveClass method on the stream to get the local class. If the class cannot be resolved, throw a ClassNotFoundException. Return the resulting ObjectStreamClass object.

  7. If the object in the stream is a String, read its length information followed by the contents of the string encoded in modified UTF-8. For details, refer to Section 6.2, "Stream Elements". Add the String and its handle to the set of known objects, and proceed to Step 123.

  8. If the object in the stream is an array, read its ObjectStreamClass and the length of the array. Allocate the array, and add it and its handle in the set of known objects. Read each element using the appropriate method for its type and assign it to the array. Proceed to Step 123.

  9. If the object in the stream is an enum constant, read its ObjectStreamClass and the enum constant name. If the ObjectStreamClass represents a class that is not an enum type, an InvalidClassException is thrown. Obtain a reference to the enum constant by calling the java.lang.Enum.valueOf method, passing the enum type bound to the received ObjectStreamClass along with the received name as arguments. If the valueOf method throws an IllegalArgumentException, an InvalidObjectException is thrown with the IllegalArgumentException as its cause. Add the enum constant and its handle in the set of known objects, and proceed to Step 123.

  10. For all other objects, the ObjectStreamClass of the object is read from the stream. The local class for that ObjectStreamClass is retrieved. The class must be serializable or externalizable, and must not be an enum type. If the class does not satisfy these criteria, an InvalidClassException is thrown.

  1. If the class is a record class. A handle, with an initial value of null, is added to the set of known objects.

    The record object will be constructed by an invocation of its canonical constructor. The canonical constructor for record class, R, is found by, first building a method descriptor from the number, order, and declared types of the record R's components, as returned by R::getRecordComponents; and then locating R's declared constructor matching the descriptor. If the canonical constructor cannot be found, an InvalidClassException is thrown.

    The contents are restored as follows:

    1. Read and restore all the field values from the stream. Match the stream fields to that of the appropriate constructor parameter, used to initialize the record component. The matching is based on the equality of the name of the stream field and the name of the record component, their names must be identical. That is, for each record component, as returned by Class::getRecordComponents, first determine the components's name, n; and then find the stream value for the stream field whose name equals n. The concrete type of a stream value for a matched field, must be assignable (Class::isAssignableFrom) to that of the type of its matched record component, or else a ClassCastException will be thrown -- all matches are type-checked. Stream fields not matched are effectively discarded.

    2. Invoke the record 's canonical constructor, with the matched stream field values. The stream field values are passed in the corresponding record component positions of the constructor parameters. Unmatched components have the default value appropriate for their type passed. If the constructor invocation throws an exception, an InvalidObjectException is thrown with that exception as its cause. Otherwise, the newly created record object is assigned to its handle of known objects. Proceed to Step 13.

112. An instance of the class is allocated. The instance and its handle are added to the set of known objects. The contents restored appropriately:

a.  For serializable objects, the no-arg constructor for the first
    non-serializable supertype is run. For serializable classes, the fields
    are initialized to the default value appropriate for its type. Then the
    fields of each class are restored by calling class-specific
    `readObject` methods, or if these are not defined, by calling the
    `defaultReadObject` method. Note that field initializers and
    constructors are not executed for serializable classes during
    deserialization. In the normal case, the version of the class that
    wrote the stream will be the same as the class reading the stream. In
    this case, all of the supertypes of the object in the stream will match
    the supertypes in the currently-loaded class. If the version of the
    class that wrote the stream had different supertypes than the loaded
    class, the `ObjectInputStream` must be more careful about restoring or
    initializing the state of the differing classes. It must step through
    the classes, matching the available data in the stream with the classes
    of the object being restored. Data for classes that occur in the
    stream, but do not occur in the object, is discarded. For classes that
    occur in the object, but not in the stream, the class fields are set to
    default values by default serialization.

b.  For externalizable objects, the no-arg constructor for the class is run
    and then the `readExternal` method is called to restore the contents of
    the object.

123. Process potential substitutions by the class of the object and/or by a subclass of ObjectInputStream:

a.  If the class of the object is not an enum type and defines the
    appropriate `readResolve` method, the method is called to allow the
    object to replace itself.

b.  Then if previously enabled by `enableResolveObject,` the
    `resolveObject` method is called to allow subclasses of the stream to
    examine and replace the object. If the previous step did replace the
    original object, the `resolveObject` method is called with the
    replacement object. If a replacement took place, the table of known
    objects is updated so the replacement object is associated with the
    handle. The replacement object is then returned from `readObject`.

All of the methods for reading primitives types only consume bytes from the block data records in the stream. If a read for primitive data occurs when the next item in the stream is an object, the read methods return -1 or the EOFException as appropriate. The value of a primitive type is read by a DataInputStream from the block data record.

The exceptions thrown reflect errors during the traversal or exceptions that occur on the underlying stream. If any exception is thrown, the underlying stream is left in an unknown and unusable state.

When the reset token occurs in the stream, all of the state of the stream is discarded. The set of known objects is cleared.

When the exception token occurs in the stream, the exception is read and a new WriteAbortedException is thrown with the terminating exception as an argument. The stream context is reset as described earlier.

The readUnshared method is used to read "unshared" objects from the stream. This method is identical to readObject, except that it prevents subsequent calls to readObject and readUnshared from returning additional references to the deserialized instance returned by the original call to readUnshared. Specifically:

Deserializing an object via readUnshared invalidates the stream handle associated with the returned object. Note that this in itself does not always guarantee that the reference returned by readUnshared is unique; the deserialized object may define a readResolve method which returns an object visible to other parties, or readUnshared may return a Class object or enum constant obtainable elsewhere in the stream or through external means. If the deserialized object defines a readResolve method and the invocation of that method returns an array, then readUnshared returns a shallow clone of that array; this guarantees that the returned array object is unique and cannot be obtained a second time from an invocation of readObject or readUnshared on the ObjectInputStream, even if the underlying data stream has been manipulated.

The defaultReadObject method is used to read the fields and object from the stream. It uses the class descriptor in the stream to read the fields in the canonical order by name and type from the stream. The values are assigned to the matching fields by name in the current class. Details of the versioning mechanism can be found in Section 5.5, "Compatible Java Type Evolution". Any field of the object that does not appear in the stream is set to its default value. Values that appear in the stream, but not in the object, are discarded. This occurs primarily when a later version of a class has written additional fields that do not occur in the earlier version. This method may only be called from the readObject method while restoring the fields of a class. When called at any other time, the NotActiveException is thrown.

The readFields method reads the values of the serializable fields from the stream and makes them available via the GetField class. The readFields method is only callable from within the readObject method of a serializable class. It cannot be called more than once or if defaultReadObject has been called. The GetFields object uses the current object's ObjectStreamClass to verify the fields that can be retrieved for this class. The GetFields object returned by readFields is only valid during this call to the classes readObject method. The fields may be retrieved in any order. Additional data may only be read directly from stream after readFields has been called.

The registerValidation method can be called to request a callback when the entire graph has been restored but before the object is returned to the original caller of readObject. The order of validate callbacks can be controlled using the priority. Callbacks registered with higher values are called before those with lower values. The object to be validated must support the ObjectInputValidation interface and implement the validateObject method. It is only correct to register validations during a call to a class's readObject method. Otherwise, a NotActiveException is thrown. If the callback object supplied to registerValidation is null, an InvalidObjectException is thrown.

Starting with the Java SDK, Standard Edition, v1.3, the readClassDescriptor method is used to read in all ObjectStreamClass objects. readClassDescriptor is called when the ObjectInputStream expects a class descriptor as the next item in the serialization stream. Subclasses of ObjectInputStream may override this method to read in class descriptors that have been written in non-standard formats (by subclasses of ObjectOutputStream which have overridden the writeClassDescriptor method). By default, this method reads class descriptors according to the format described in Section 6.4, "Grammar for the Stream Format".

The resolveClass method is called while a class is being deserialized, and after the class descriptor has been read. Subclasses may extend this method to read other information about the class written by the corresponding subclass of ObjectOutputStream. The method must find and return the class with the given name and serialVersionUID. The default implementation locates the class by calling the class loader of the closest caller of readObject that has a class loader. If the class cannot be found ClassNotFoundException should be thrown. Prior to JDK 1.1.6, the resolveClass method was required to return the same fully qualified class name as the class name in the stream. In order to accommodate package renaming across releases, method resolveClass only needs to return a class with the same base class name and SerialVersionUID in JDK 1.1.6 and later versions.

The resolveObject method is used by trusted subclasses to monitor or substitute one object for another during deserialization. Resolving objects must be enabled explicitly by calling enableResolveObject before calling readObject for the first object to be resolved. Once enabled, resolveObject is called once for each serializable object just prior to the first time it is being returned from readObject. Note that the resolveObject method is not called for objects of the specially handled classes, Class, ObjectStreamClass, String, and arrays. A subclass's implementation of resolveObject may return a substitute object that will be assigned or returned instead of the original. The object returned must be of a type that is consistent and assignable to every reference of the original object or else a ClassCastException will be thrown. All assignments are type-checked. All references in the stream to the original object will be replaced by references to the substitute object.

The enableResolveObject method is called by trusted subclasses of ObjectOutputStream to enable the monitoring or substitution of one object for another during deserialization. Replacing objects is disabled until enableResolveObject is called with a true value. It may thereafter be disabled by setting it to false. The previous setting is returned. The enableResolveObject method checks if the stream has permission to request substitution during serialization. To ensure that the private state of objects is not unintentionally exposed, only trusted streams may use resolveObject. Trusted classes are those classes with a class loader equal to null or belong to a security protection domain that provides permission to enable substitution.

If the subclass of ObjectInputStream is not considered part of the system domain, a line has to be added to the security policy file to provide to a subclass of ObjectInputStream permission to call enableResolveObject. The SerializablePermission to add is "enableSubstitution". AccessControlException is thrown if the protection domain of the subclass of ObjectStreamClass does not have permission to "enableSubstitution" by calling enableResolveObject. See the document Java Security Architecture (JDK 1.2) for additional information about the security model.

The readStreamHeader method reads and verifies the magic number and version of the stream. If they do not match, the StreamCorruptedMismatch is thrown.

To override the implementation of deserialization, a subclass of ObjectInputStream should call the protected no-arg ObjectInputStream, constructor. There is a security check within the no-arg constructor for SerializablePermission "enableSubclassImplementation" to ensure that only trusted classes are allowed to override the default implementation. This constructor does not allocate any private data for ObjectInputStream and sets a flag that indicates that the final readObject method should invoke the readObjectOverride method and return. All other ObjectInputStream methods are not final and can be directly overridden by the subclass.

3.2 The ObjectInputStream.GetField Class

The class ObjectInputStream.GetField provides the API for getting the values of serializable fields. The protocol of the stream is the same as used by defaultReadObject. Using readFields to access the serializable fields does not change the format of the stream. It only provides an alternate API to access the values which does not require the class to have the corresponding non-transient and non-static fields for each named serializable field. The serializable fields are those declared using serialPersistentFields or if it is not declared the non-transient and non-static fields of the object. When the stream is read the available serializable fields are those written to the stream when the object was serialized. If the class that wrote the stream is a different version not all fields will correspond to the serializable fields of the current class. The available fields can be retrieved from the ObjectStreamClass of the GetField object.

The getObjectStreamClass method returns an ObjectStreamClass object representing the class in the stream. It contains the list of serializable fields.

The defaulted method returns true if the field is not present in the stream. An IllegalArgumentException is thrown if the requested field is not a serializable field of the current class.

Each get method returns the specified serializable field from the stream. I/O exceptions will be thrown if the underlying stream throws an exception. An IllegalArgumentException is thrown if the name or type does not match the name and type of an field serializable field of the current class. The default value is returned if the stream does not contain an explicit value for the field.

3.3 The ObjectInputValidation Interface

This interface allows an object to be called when a complete graph of objects has been deserialized. If the object cannot be made valid, it should throw the ObjectInvalidException. Any exception that occurs during a call to validateObject will terminate the validation process, and the InvalidObjectException will be thrown.

package java.io;

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

3.4 The readObject Method

For serializable objects, the readObject method allows a class to control the deserialization of its own fields. Here is its signature:

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

Each subclass of a serializable object may define its own readObject method. If a class does not implement the method, the default serialization provided by defaultReadObject will be used. When implemented, the class is only responsible for restoring its own fields, not those of its supertypes or subtypes.

The readObject method of the class, if implemented, is responsible for restoring the state of the class. The values of every field of the object whether transient or not, static or not are set to the default value for the fields type. Either ObjectInputStream's defaultReadObject or readFields method must be called once (and only once) before reading any optional data written by the corresponding writeObject method; even if no optional data is read, defaultReadObject or readFields must still be invoked once. If the readObject method of the class attempts to read more data than is present in the optional part of the stream for this class, the stream will return -1 for bytewise reads, throw an EOFException for primitive data reads (e.g., readInt, readFloat), or throw an OptionalDataException with the eof field set to true for object reads.

The responsibility for the format, structure, and versioning of the optional data lies completely with the class. The @serialData javadoc tag within the javadoc comment for the readObject method should be used to document the format and structure of the optional data.

If the class being restored is not present in the stream being read, then its readObjectNoData method, if defined, is invoked (instead of readObject); otherwise, its fields are initialized to the appropriate default values. For further detail, see Section 3.5, "The readObjectNoData Method".

Reading an object from the ObjectInputStream is analogous to creating a new object. Just as a new object's constructors are invoked in the order from the superclass to the subclass, an object being read from a stream is deserialized from superclass to subclass. The readObject or readObjectNoData method is called instead of the constructor for each Serializable subclass during deserialization.

One last similarity between a constructor and a readObject method is that both provide the opportunity to invoke a method on an object that is not fully constructed. Any overridable (neither private, static nor final) method called while an object is being constructed can potentially be overridden by a subclass. Methods called during the construction phase of an object are resolved by the actual type of the object, not the type currently being initialized by either its constructor or readObject/readObjectNoData method. Therefore, calling an overridable method from within a readObject or readObjectNoData method may result in the unintentional invocation of a subclass method before the superclass has been fully initialized.

3.5 The readObjectNoData Method

For serializable objects, the readObjectNoData method allows a class to control the initialization of its own fields in the event that a subclass instance is deserialized and the serialization stream does not list the class in question as a superclass of the deserialized object. This may occur in cases where the receiving party uses a different version of the deserialized instance's class than the sending party, and the receiver's version extends classes that are not extended by the sender's version. This may also occur if the serialization stream has been tampered; hence, readObjectNoData is useful for initializing deserialized objects properly despite a "hostile" or incomplete source stream.

private void readObjectNoData() throws ObjectStreamException;

Each serializable class may define its own readObjectNoData method. If a serializable class does not define a readObjectNoData method, then in the circumstances listed above the fields of the class will be initialized to their default values (as listed in The Java Language Specification); this behavior is consistent with that of ObjectInputStream prior to version 1.4 of the Java 2 SDK, Standard Edition, when support for readObjectNoData methods was introduced. If a serializable class does define a readObjectNoData method and the aforementioned conditions arise, then readObjectNoData will be invoked at the point during deserialization when a class-defined readObject method would otherwise be called had the class in question been listed by the stream as a superclass of the instance being deserialized.

3.6 The readExternal Method

Objects implementing java.io.Externalizable must implement the readExternal method to restore the entire state of the object. It must coordinate with its superclasses to restore their state. All of the methods of ObjectInput are available to restore the object's primitive typed fields and object fields.

public void readExternal(ObjectInput stream)
    throws IOException;

Note: The readExternal method is public, and it raises the risk of a client being able to overwrite an existing object from a stream. The class may add its own checks to insure that this is only called when appropriate.

A new stream protocol version has been introduced in JDK 1.2 to correct a problem with Externalizable objects. The old definition of Externalizable objects required the local virtual machine to find a readExternal method to be able to properly read an Externalizable object from the stream. The new format adds enough information to the stream protocol so serialization can skip an Externalizable object when the local readExternal method is not available. Due to class evolution rules, serialization must be able to skip an Externalizable object in the input stream if there is not a mapping for the object using the local classes.

An additional benefit of the new Externalizable stream format is that ObjectInputStream can detect attempts to read more External data than is available, and can also skip by any data that is left unconsumed by a readExternal method. The behavior of ObjectInputStream in response to a read past the end of External data is the same as the behavior when a class-defined readObject method attempts to read past the end of its optional data: bytewise reads will return -1, primitive reads will throw EOFExceptions, and object reads will throw OptionalDataExceptions with the eof field set to true.

Due to the format change, JDK 1.1.6 and earlier releases are not able to read the new format. StreamCorruptedException is thrown when JDK 1.1.6 or earlier attempts to read an Externalizable object from a stream written in PROTOCOL_VERSION_2. Compatibility issues are discussed in more detail in Section 6.3, "Stream Protocol Versions".

3.7 The readResolve Method

For Serializable and Externalizable classes, the readResolve method allows a class to replace/resolve the object read from the stream before it is returned to the caller. By implementing the readResolve method, a class can directly control the types and instances of its own instances being deserialized. The method is defined as follows:

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

The readResolve method is called when ObjectInputStream has read an object from the stream and is preparing to return it to the caller. ObjectInputStream checks whether the class of the object defines the readResolve method. If the method is defined, the readResolve method is called to allow the object in the stream to designate the object to be returned. The object returned should be of a type that is compatible with all uses. If it is not compatible, a ClassCastException will be thrown when the type mismatch is discovered.

For example, a Symbol class could be created for which only a single instance of each symbol binding existed within a virtual machine. The readResolve method would be implemented to determine if that symbol was already defined and substitute the preexisting equivalent Symbol object to maintain the identity constraint. In this way the uniqueness of Symbol objects can be maintained across serialization.

Note: The readResolve method is not invoked on the object until the object is fully constructed, so any references to this object in its object graph will not be updated to the new object nominated by readResolve. However, during the serialization of an object with the writeReplace method, all references to the original object in the replacement object's object graph are replaced with references to the replacement object. Therefore in cases where an object being serialized nominates a replacement object whose object graph has a reference to the original object, deserialization will result in an incorrect graph of objects. Furthermore, if the reference types of the object being read (nominated by writeReplace) and the original object are not compatible, the construction of the object graph will raise a ClassCastException.


4 - Class Descriptors

4.1 The ObjectStreamClass Class

The ObjectStreamClass provides information about classes that are saved in a Serialization stream. The descriptor provides the fully-qualified name of the class and its serialization version UID. A SerialVersionUID identifies the unique original class version for which this class is capable of writing streams and from which it can read.

package java.io;

public class ObjectStreamClass
{
    public static ObjectStreamClass lookup(Class cl);

        public static ObjectStreamClass lookupAny(Class cl);

    public String getName();

    public Class forClass();

    public ObjectStreamField[] getFields();

    public long getSerialVersionUID();

    public String toString();
}

The lookup method returns the ObjectStreamClass descriptor for the specified class in the virtual machine. If the class has defined serialVersionUID it is retrieved from the class. If the serialVersionUID is not defined by the class, it is computed from the definition of the class in the virtual machine. If the specified class is not serializable or externalizable, null is returned.

The lookupAny method behaves like the lookup method, except that it returns the descriptor for any class, regardless of whether it implements Serializable. The serialVersionUID of a class that does not implement Serializable is 0L.

The getName method returns the name of the class, in the same format that is used by the Class.getName method.

The forClass method returns the Class in the local virtual machine if one was found by ObjectInputStream.resolveClass method. Otherwise, it returns null.

The getFields method returns an array of ObjectStreamField objects that represent the serializable fields of this class.

The getSerialVersionUID method returns the serialVersionUID of this class. Refer to Section 4.6, "Stream Unique Identifiers". If not specified by the class, the value returned is a hash computed from the class's name, interfaces, methods, and fields using the Secure Hash Algorithm (SHA) as defined by the National Institute of Standards.

The toString method returns a printable representation of the class descriptor including the name of the class and the serialVersionUID.

4.2 Dynamic Proxy Class Descriptors

ObjectStreamClass descriptors are also used to provide information about dynamic proxy classes (e.g., classes obtained via calls to the getProxyClass method of java.lang.reflect.Proxy) saved in a serialization stream. A dynamic proxy class itself has no serializable fields and a serialVersionUID of 0L. In other words, when the Class object for a dynamic proxy class is passed to the static lookup method of ObjectStreamClass, the returned ObjectStreamClass instance will have the following properties:

4.3 Serialized Form

The serialized form of an ObjectStreamClass instance depends on whether or not the Class object it represents is serializable, externalizable, or a dynamic proxy class.

When an ObjectStreamClass instance that does not represent a dynamic proxy class is written to the stream, it writes the class name and serialVersionUID, flags, and the number of fields. Depending on the class, additional information may be written:

When an ObjectOutputStream serializes the ObjectStreamClass descriptor for a dynamic proxy class, as determined by passing its Class object to the isProxyClass method of java.lang.reflect.Proxy, it writes the number of interfaces that the dynamic proxy class implements, followed by the interface names. Interfaces are listed in the order that they are returned by invoking the getInterfaces method on the Class object of the dynamic proxy class.

The serialized representations of ObjectStreamClass descriptors for dynamic proxy classes and non-dynamic proxy classes are differentiated through the use of different typecodes (TC_PROXYCLASSDESC and TC_CLASSDESC, respectively); for a more detailed specification of the grammar, see Section 6.4, "Grammar for the Stream Format".

4.4 The ObjectStreamField Class

An ObjectStreamField represents a serializable field of a serializable class. The serializable fields of a class can be retrieved from the ObjectStreamClass.

The special static serializable field, serialPersistentFields, is an array of ObjectStreamField components that is used to override the default serializable fields.

package java.io;

public class ObjectStreamField implements Comparable {

    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 objects are used to specify the serializable fields of a class or to describe the fields present in a stream. Its constructors accept arguments describing the field to represent: a string specifying the name of the field, a Class object specifying the type of the field, and a boolean flag (implicitly false for the two-argument constructor) indicating whether or not values of the represented field should be read and written as "unshared" objects if default serialization/deserialization is in use (see the descriptions of the ObjectInputStream.readUnshared and ObjectOutputStream.writeUnshared methods in Section 3.1, "The ObjectInputStream Class" and Section 2.1, "The ObjectOutputStream Class", respectively).

The getName method returns the name of the serializable field.

The getType method returns the type of the field.

The getTypeString method returns the type signature of the field.

The getTypeCode method returns a character encoding of the field type ('B' for byte, 'C' for char, 'D' for double, 'F' for float, 'I' for int, 'J' for long, 'L' for non-array object types, 'S' for short, 'Z' for boolean, and '[' for arrays).

The isPrimitive method returns true if the field is of primitive type, or false otherwise.

The isUnshared method returns true if values of the field should be written as "unshared" objects, or false otherwise.

The getOffset method returns the offset of the field's value within instance data of the class defining the field.

The setOffset method allows ObjectStreamField subclasses to modify the offset value returned by the getOffset method.

The compareTo method compares ObjectStreamFields for use in sorting. Primitive fields are ranked as "smaller" than non-primitive fields; fields otherwise equal are ranked alphabetically.

The toString method returns a printable representation with name and type.

4.5 Inspecting Serializable Classes

The program serialver can be used to find out if a class is serializable and to get its serialVersionUID.

When invoked on the command line with one or more class names, serialver prints the serialVersionUID for each class in a form suitable for copying into an evolving class. When invoked with no arguments, it prints a usage line.

4.6 Stream Unique Identifiers

Minor changes for record SerialVersionUID

Each versioned class must identify the original class version for which it is capable of writing streams and from which it can read. For example, a versioned class must declare:

private static final long serialVersionUID = 3487495895819393L;

The stream-unique identifier is a 64-bit hash of the class name, interface class names, methods, and fields. The value must be declared in all versions of a class except the first. It may be declared in the original class but is not required. The value is fixed for all compatible classes. If the SUID is not declared for a class, the value defaults to the hash for that class. The serialVersionUID for dynamic proxy classes and enum types always have the value 0L. Array classes cannot declare an explicit serialVersionUID, so they always have the default computed value, but the requirement for matching serialVersionUID values is waived for array classes. Record classes have a default serialVersionUID value of 0L, but can declare an explicit serialVersionUID. The requirement for matching serialVersionUID values is waived for record classes.

Note: It is strongly recommended that all serializable classes explicitly declare serialVersionUID values, since the default serialVersionUID computation is highly sensitive to class details that may vary depending on compiler implementations, and can thus result in unexpected serialVersionUID conflicts during deserialization, causing deserialization to fail.

The initial version of an Externalizable class must output a stream data format that is extensible in the future. The initial version of the method readExternal has to be able to read the output format of all future versions of the method writeExternal.

The serialVersionUID is computed using the signature of a stream of bytes that reflect the class definition. The National Institute of Standards and Technology (NIST) Secure Hash Algorithm (SHA-1) is used to compute a signature for the stream. The first two 32-bit quantities are used to form a 64-bit hash. A java.lang.DataOutputStream is used to convert primitive data types to a sequence of bytes. The values input to the stream are defined by the Java Virtual Machine (VM) specification for classes. Class modifiers may include the ACC_PUBLIC, ACC_FINAL, ACC_INTERFACE, and ACC_ABSTRACT flags; other flags are ignored and do not affect serialVersionUID computation. Similarly, for field modifiers, only the ACC_PUBLIC, ACC_PRIVATE, ACC_PROTECTED, ACC_STATIC, ACC_FINAL, ACC_VOLATILE, and ACC_TRANSIENT flags are used when computing serialVersionUID values. For constructor and method modifiers, only the ACC_PUBLIC, ACC_PRIVATE, ACC_PROTECTED, ACC_STATIC, ACC_FINAL, ACC_SYNCHRONIZED, ACC_NATIVE, ACC_ABSTRACT and ACC_STRICT flags are used. Names and descriptors are written in the format used by the java.io.DataOutputStream.writeUTF method.

The sequence of items in the stream is as follows:

  1. The class name.

  2. The class modifiers written as a 32-bit integer.

  3. The name of each interface sorted by name.

  4. For each field of the class sorted by field name (except private static and private transient fields:

    1. The name of the field.

    2. The modifiers of the field written as a 32-bit integer.

    3. The descriptor of the field.

  5. If a class initializer exists, write out the following:

    1. The name of the method, <clinit>.

    2. The modifier of the method, java.lang.reflect.Modifier.STATIC, written as a 32-bit integer.

    3. The descriptor of the method, ()V.

  6. For each non-private constructor sorted by method name and signature:

    1. The name of the method, <init>.

    2. The modifiers of the method written as a 32-bit integer.

    3. The descriptor of the method.

  7. For each non-private method sorted by method name and signature:

    1. The name of the method.

    2. The modifiers of the method written as a 32-bit integer.

    3. The descriptor of the method.

  8. The SHA-1 algorithm is executed on the stream of bytes produced by DataOutputStream and produces five 32-bit values sha[0..4].

  9. The hash value is assembled from the first and second 32-bit values of the SHA-1 message digest. If the result of the message digest, the five 32-bit words H0 H1 H2 H3 H4, is in an array of five int values named sha, the hash value would be computed as follows:

      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;

5 - Versioning of Serializable Objects

5.1 Overview

When Java objects use serialization to save state in files, or as blobs in databases, the potential arises that the version of a class reading the data is different than the version that wrote the data.

Versioning raises some fundamental questions about the identity of a class, including what constitutes a compatible change. A compatible change is a change that does not affect the contract between the class and its callers.

This section describes the goals, assumptions, and a solution that attempts to address this problem by restricting the kinds of changes allowed and by carefully choosing the mechanisms.

The proposed solution provides a mechanism for "automatic" handling of classes that evolve by adding fields and adding classes. Serialization will handle versioning without class-specific methods to be implemented for each version. The stream format can be traversed without invoking class-specific methods.

5.2 Goals

The goals are to:

5.3 Assumptions

The assumptions are that:

5.4 Who's Responsible for Versioning of Streams

In the evolution of classes, it is the responsibility of the evolved (later version) class to maintain the contract established by the nonevolved class. This takes two forms. First, the evolved class must not break the existing assumptions about the interface provided by the original version, so that the evolved class can be used in place of the original. Secondly, when communicating with the original (or previous) versions, the evolved class must provide sufficient and equivalent information to allow the earlier version to continue to satisfy the nonevolved contract.

Private serialization protocol and contract with supertype relationships between evolved and nonevolved classes and their instances
Private serialization protocol and contract with supertype relationships between evolved and nonevolved classes and their instances

For the purposes of the discussion here, each class implements and extends the interface or contract defined by its supertype. New versions of a class, for example foo', must continue to satisfy the contract for foo and may extend the interface or modify its implementation.

Communication between objects via serialization is not part of the contract defined by these interfaces. Serialization is a private protocol between the implementations. It is the responsibility of the implementations to communicate sufficiently to allow each implementation to continue to satisfy the contract expected by its clients.

5.5 Compatible Java Type Evolution

The Java Language Specification discusses binary compatibility of Java classes as those classes evolve. Most of the flexibility of binary compatibility comes from the use of late binding of symbolic references for the names of classes, interfaces, fields, methods, and so on.

The following are the principle aspects of the design for versioning of serialized object streams.

5.6 Type Changes Affecting Serialization

With these concepts, we can now describe how the design will cope with the different cases of an evolving class. The cases are described in terms of a stream written by some version of a class. When the stream is read back by the same version of the class, there is no loss of information or functionality. The stream is the only source of information about the original class. Its class descriptions, while a subset of the original class description, are sufficient to match up the data in the stream with the version of the class being reconstituted.

The descriptions are from the perspective of the stream being read in order to reconstitute either an earlier or later version of the class. In the parlance of RPC systems, this is a "receiver makes right" system. The writer writes its data in the most suitable form and the receiver must interpret that information to extract the parts it needs and to fill in the parts that are not available.

5.6.1 Incompatible Changes

Incompatible changes to classes are those changes for which the guarantee of interoperability cannot be maintained. The incompatible changes that may occur while evolving a class are:

5.6.2 Compatible Changes

Addition of class<->record compatible changes

The compatible changes to a class are handled as follows:


6 - Object Serialization Stream Protocol

6.1 Overview

The stream format satisfies the following design goals:

6.2 Stream Elements

A basic structure is needed to represent objects in a stream. Each attribute of the object needs to be represented: its classes, its fields, and data written and later read by class-specific methods. The representation of objects in the stream can be described with a grammar. There are special representations for null objects, new objects, classes, arrays, strings, and back references to any object already in the stream. Each object written to the stream is assigned a handle that is used to refer back to the object. Handles are assigned sequentially starting from 0x7E0000. The handles restart at 0x7E0000 when the stream is reset.

A class object is represented by the following:

An ObjectStreamClass object for a Class that is not a dynamic proxy class is represented by the following:

An ObjectStreamClass object for a dynamic proxy class is represented by the following:

The representation of String objects consists of length information followed by the contents of the string encoded in modified UTF-8. The modified UTF-8 encoding is the same as used in the Java Virtual Machine and in the java.io.DataInput and DataOutput interfaces; it differs from standard UTF-8 in the representation of supplementary characters and of the null character. The form of the length information depends on the length of the string in modified UTF-8 encoding. If the modified UTF-8 encoding of the given String is less than 65536 bytes in length, the length is written as 2 bytes representing an unsigned 16-bit integer. Starting with the Java 2 platform, Standard Edition, v1.3, if the length of the string in modified UTF-8 encoding is 65536 bytes or more, the length is written in 8 bytes representing a signed 64-bit integer. The typecode preceding the String in the serialization stream indicates which format was used to write the String.

Arrays are represented by the following:

Enum constants are represented by the following:

New objects in the stream are represented by the following:

All primitive data written by classes is buffered and wrapped in block-data records, regardless if the data is written to the stream within a writeObject method or written directly to the stream from outside a writeObject method. This data can only be read by the corresponding readObject methods or be read directly from the stream. Objects written by the writeObject method terminate any previous block-data record and are written either as regular objects or null or back references, as appropriate. The block-data records allow error recovery to discard any optional data. When called from within a class, the stream can discard any data or objects until the endBlockData.

6.3 Stream Protocol Versions

It was necessary to make a change to the serialization stream format in JDK 1.2 that is not backwards compatible to all minor releases of JDK 1.1. To provide for cases where backwards compatibility is required, a capability has been added to indicate what PROTOCOL_VERSION to use when writing a serialization stream. The method ObjectOutputStream.useProtocolVersion takes as a parameter the protocol version to use to write the serialization stream.

The Stream Protocol Versions are as follows:

JDK 1.2 defaults to writing PROTOCOL_VERSION_2.

JDK 1.1 defaults to writing PROTOCOL_VERSION_1.

JDK 1.1.7 and greater can read both versions.

Releases prior to JDK 1.1.7 can only read PROTOCOL_VERSION_1.

6.4 Grammar for the Stream Format

The table below contains the grammar for the stream format. Nonterminal symbols are shown in italics. Terminal symbols in a fixed width font. Definitions of nonterminals are followed by a ":". The definition is followed by one or more alternatives, each on a separate line. The following table describes the notation:

Notation Meaning
(datatype) This token has the data type specified, such as byte.
token[n] A predefined number of occurrences of the token, that is an array.
x0001 A literal value expressed in hexadecimal. The number of hex digits reflects the size of the value.
<xxx> A value read from the stream used to indicate the length of an array.

Note that the symbol (utf) is used to designate a string written using 2-byte length information, and (long-utf) is used to designate a string written using 8-byte length information. For details, refer to Section 6.2, "Stream Elements".

6.4.1 Rules of the Grammar

A Serialized stream is represented by any stream satisfying the stream rule.

stream:
  magic version contents

contents:
  content
  contents content

content:
  object
  blockdata

object:
  newObject
  newClass
  newArray
  newString
  newEnum
  newClassDesc
  prevObject
  nullReference
  exception
  TC_RESET

newClass:
  TC_CLASS classDesc newHandle

classDesc:
  newClassDesc
  nullReference
  (ClassDesc)prevObject      // an object required to be of type ClassDesc

superClassDesc:
  classDesc

newClassDesc:
  TC_CLASSDESC className serialVersionUID newHandle classDescInfo
  TC_PROXYCLASSDESC newHandle proxyClassDescInfo

classDescInfo:
  classDescFlags fields classAnnotation superClassDesc

className:
  (utf)

serialVersionUID:
  (long)

classDescFlags:
  (byte)                  // Defined in Terminal Symbols and Constants

proxyClassDescInfo:
  (int)<count> proxyInterfaceName[count] classAnnotation
      superClassDesc

proxyInterfaceName:
  (utf)

fields:
  (short)<count> fieldDesc[count]

fieldDesc:
  primitiveDesc
  objectDesc

primitiveDesc:
  prim_typecode fieldName

objectDesc:
  obj_typecode fieldName className1

fieldName:
  (utf)

className1:
  (String)object             // String containing the field's type,
                             // in field descriptor format

classAnnotation:
  endBlockData
  contents endBlockData      // contents written by annotateClass

prim_typecode:
  'B'       // byte
  'C'       // char
  'D'       // double
  'F'       // float
  'I'       // integer
  'J'       // long
  'S'       // short
  'Z'       // boolean

obj_typecode:
  '['       // array
  'L'       // object

newArray:
  TC_ARRAY classDesc newHandle (int)<size> values[size]

newObject:
  TC_OBJECT classDesc newHandle classdata[]  // data for each class

classdata:
  nowrclass                 // SC_SERIALIZABLE & classDescFlag &&
                            // !(SC_WRITE_METHOD & classDescFlags)
  wrclass objectAnnotation  // SC_SERIALIZABLE & classDescFlag &&
                            // SC_WRITE_METHOD & classDescFlags
  externalContents          // SC_EXTERNALIZABLE & classDescFlag &&
                            // !(SC_BLOCKDATA  & classDescFlags
  objectAnnotation          // SC_EXTERNALIZABLE & classDescFlag&&
                            // SC_BLOCKDATA & classDescFlags

nowrclass:
  values                    // fields in order of class descriptor

wrclass:
  nowrclass

objectAnnotation:
  endBlockData
  contents endBlockData     // contents written by writeObject
                            // or writeExternal PROTOCOL_VERSION_2.

blockdata:
  blockdatashort
  blockdatalong

blockdatashort:
  TC_BLOCKDATA (unsigned byte)<size> (byte)[size]

blockdatalong:
  TC_BLOCKDATALONG (int)<size> (byte)[size]

endBlockData:
  TC_ENDBLOCKDATA

externalContent:         // Only parseable by readExternal
  (bytes)                // primitive data
   object

externalContents:         // externalContent written by
  externalContent         // writeExternal in PROTOCOL_VERSION_1.
  externalContents externalContent

newString:
  TC_STRING newHandle (utf)
  TC_LONGSTRING newHandle (long-utf)

newEnum:
  TC_ENUM classDesc newHandle enumConstantName

enumConstantName:
  (String)object

prevObject:
  TC_REFERENCE (int)handle

nullReference:
  TC_NULL

exception:
  TC_EXCEPTION reset (Throwable)object reset

magic:
  STREAM_MAGIC

version:
  STREAM_VERSION

values:          // The size and types are described by the
                 // classDesc for the current object

newHandle:       // The next number in sequence is assigned
                 // to the object being serialized or deserialized

reset:           // The set of known objects is discarded
                 // so the objects of the exception do not
                 // overlap with the previously sent objects
                 // or with objects that may be sent after
                 // the exception

6.4.2 Terminal Symbols and Constants

The following symbols in java.io.ObjectStreamConstants define the terminal and constant values expected in a stream.

final static short STREAM_MAGIC = (short)0xaced;
final static short STREAM_VERSION = 5;
final static byte TC_NULL = (byte)0x70;
final static byte TC_REFERENCE = (byte)0x71;
final static byte TC_CLASSDESC = (byte)0x72;
final static byte TC_OBJECT = (byte)0x73;
final static byte TC_STRING = (byte)0x74;
final static byte TC_ARRAY = (byte)0x75;
final static byte TC_CLASS = (byte)0x76;
final static byte TC_BLOCKDATA = (byte)0x77;
final static byte TC_ENDBLOCKDATA = (byte)0x78;
final static byte TC_RESET = (byte)0x79;
final static byte TC_BLOCKDATALONG = (byte)0x7A;
final static byte TC_EXCEPTION = (byte)0x7B;
final static byte TC_LONGSTRING = (byte) 0x7C;
final static byte TC_PROXYCLASSDESC = (byte) 0x7D;
final static byte TC_ENUM = (byte) 0x7E;
final static  int   baseWireHandle = 0x7E0000;

The flag byte classDescFlags may include values of

final static byte SC_WRITE_METHOD = 0x01; //if SC_SERIALIZABLE
final static byte SC_BLOCK_DATA = 0x08;    //if SC_EXTERNALIZABLE
final static byte SC_SERIALIZABLE = 0x02;
final static byte SC_EXTERNALIZABLE = 0x04;
final static byte SC_ENUM = 0x10;

The flag SC_WRITE_METHOD is set if the Serializable class writing the stream had a writeObject method that may have written additional data to the stream. In this case a TC_ENDBLOCKDATA marker is always expected to terminate the data for that class.

The flag SC_BLOCKDATA is set if the Externalizable class is written into the stream using STREAM_PROTOCOL_2. By default, this is the protocol used to write Externalizable objects into the stream in JDK 1.2. JDK 1.1 writes STREAM_PROTOCOL_1.

The flag SC_SERIALIZABLE is set if the class that wrote the stream extended java.io.Serializable but not java.io.Externalizable, the class reading the stream must also extend java.io.Serializable and the default serialization mechanism is to be used.

The flag SC_EXTERNALIZABLE is set if the class that wrote the stream extended java.io.Externalizable, the class reading the data must also extend Externalizable and the data will be read using its writeExternal and readExternal methods.

The flag SC_ENUM is set if the class that wrote the stream was an enum type. The receiver's corresponding class must also be an enum type. Data for constants of the enum type will be written and read as described in Section 1.12, "Serialization of Enum Constants".

Example

Consider the case of an original class and two instances in a linked list:

class List implements java.io.Serializable {
    int value;
    List next;
    public static void main(String[] args) {
        try {
            List list1 = new List();
            List list2 = new List();
            list1.value = 17;
            list1.next = list2;
            list2.value = 19;
            list2.next = null;

            ByteArrayOutputStream o = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(o);
            out.writeObject(list1);
            out.writeObject(list2);
            out.flush();
            ...
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

The resulting stream contains:

    00: ac ed 00 05 73 72 00 04 4c 69 73 74 69 c8 8a 15 >....sr..Listi...<
    10: 40 16 ae 68 02 00 02 49 00 05 76 61 6c 75 65 4c >Z......I..valueL<
    20: 00 04 6e 65 78 74 74 00 06 4c 4c 69 73 74 3b 78 >..nextt..LList;x<
    30: 70 00 00 00 11 73 71 00 7e 00 00 00 00 00 13 70 >p....sq.~......p<
    40: 71 00 7e 00 03                                  >q.~..<