< prev index next >

src/java.base/share/classes/java/io/ObjectInputStream.java

Print this page
@@ -24,35 +24,39 @@
   */
  
  package java.io;
  
  import java.io.ObjectInputFilter.Config;
- import java.io.ObjectStreamClass.RecordSupport;
+ import java.io.ObjectStreamClass.ConstructorSupport;
+ import java.io.ObjectStreamClass.ClassDataSlot;
  import java.lang.System.Logger;
  import java.lang.invoke.MethodHandle;
  import java.lang.reflect.Array;
  import java.lang.reflect.InvocationHandler;
+ import java.lang.reflect.InvocationTargetException;
  import java.lang.reflect.Modifier;
  import java.lang.reflect.Proxy;
  import java.nio.charset.StandardCharsets;
  import java.security.AccessControlContext;
  import java.security.AccessController;
  import java.security.PrivilegedAction;
  import java.security.PrivilegedActionException;
  import java.security.PrivilegedExceptionAction;
  import java.util.Arrays;
- import java.util.Map;
+ import java.util.List;
+ import java.util.Locale;
  import java.util.Objects;
  
  import jdk.internal.access.JavaLangAccess;
  import jdk.internal.access.SharedSecrets;
  import jdk.internal.event.DeserializationEvent;
  import jdk.internal.misc.Unsafe;
  import jdk.internal.util.ByteArray;
  import sun.reflect.misc.ReflectUtil;
  import sun.security.action.GetBooleanAction;
  import sun.security.action.GetIntegerAction;
+ import sun.security.action.GetPropertyAction;
  
  /**
   * An ObjectInputStream deserializes primitive data and objects previously
   * written using an ObjectOutputStream.
   *

@@ -217,10 +221,12 @@
   * form.  The methods of the Externalizable interface, writeExternal and
   * readExternal, are called to save and restore the objects state.  When
   * implemented by a class they can write and read their own state using all of
   * the methods of ObjectOutput and ObjectInput.  It is the responsibility of
   * the objects to handle any versioning that occurs.
+  * Value objects cannot be `java.io.Externalizable` because value objects are
+  * immutable and `Externalizable.readExternal` is unable to modify the fields of the value.
   *
   * <p>Enum constants are deserialized 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 transmitted.  To
   * deserialize an enum constant, ObjectInputStream reads the constant name from

@@ -242,10 +248,14 @@
   * as readObject and writeObject, are ignored for serializable records. See
   * <a href="{@docRoot}/../specs/serialization/serial-arch.html#serialization-of-records">
   * <cite>Java Object Serialization Specification,</cite> Section 1.13,
   * "Serialization of Records"</a> for additional information.
   *
+  * <p>Value classes are {@linkplain Serializable} through the use of the serialization proxy pattern.
+  * See {@linkplain ObjectOutputStream##valueclass-serialization value class serialization} for details.
+  * When the proxy is deserialized it re-constructs and returns the value object.
+  *
   * @spec serialization/index.html Java Object Serialization Specification
   * @author      Mike Warres
   * @author      Roger Riggs
   * @see java.io.DataInput
   * @see java.io.ObjectOutputStream

@@ -255,10 +265,20 @@
   * @since   1.1
   */
  public class ObjectInputStream
      extends InputStream implements ObjectInput, ObjectStreamConstants
  {
+     private static final String TRACE_DEST =
+             GetPropertyAction.privilegedGetProperty("TRACE");
+ 
+     static void TRACE(String format, Object... args) {
+         if (TRACE_DEST != null) {
+             var ps = "OUT".equals(TRACE_DEST.toUpperCase(Locale.ROOT)) ? System.out : System.err;
+             ps.println(("TRACE " + format).formatted(args));
+         }
+     }
+ 
      /** handle value representing null */
      private static final int NULL_HANDLE = -1;
  
      /** marker for unshared objects in internal handle table */
      private static final Object unsharedMarker = new Object();

@@ -464,10 +484,18 @@
       *
       * <p>The deserialization filter, when not {@code null}, is invoked for
       * each object (regular or class) read to reconstruct the root object.
       * See {@link #setObjectInputFilter(ObjectInputFilter) setObjectInputFilter} for details.
       *
+      * <p>Serialization and deserialization of value classes is described in
+      * {@linkplain ObjectOutputStream##valueclass-serialization value class serialization}.
+      *
+      * @implSpec
+      * When enabled with {@code --enable-preview}, serialization and deserialization of
+      * Core Library value classes migrated from pre-JEP 401 identity classes is
+      * implementation specific.
+      *
       * <p>Exceptions are thrown for problems with the InputStream and for
       * classes that should not be deserialized.  All exceptions are fatal to
       * the InputStream and leave it in an indeterminate state; it is up to the
       * caller to ignore or recover the stream state.
       *

@@ -597,10 +625,13 @@
       *
       * <p>The deserialization filter, when not {@code null}, is invoked for
       * each object (regular or class) read to reconstruct the root object.
       * See {@link #setObjectInputFilter(ObjectInputFilter) setObjectInputFilter} for details.
       *
+      * <p>Serialization and deserialization of value classes is described in
+      * {@linkplain ObjectOutputStream##valueclass-serialization value class serialization}.
+      *
       * <p>ObjectInputStream subclasses which override this method can only be
       * constructed in security contexts possessing the
       * "enableSubclassImplementation" SerializablePermission; any attempt to
       * instantiate such a subclass without this permission will cause a
       * SecurityException to be thrown.

@@ -2250,71 +2281,134 @@
          if (cl == String.class || cl == Class.class
                  || cl == ObjectStreamClass.class) {
              throw new InvalidClassException("invalid class descriptor");
          }
  
-         Object obj;
-         try {
-             obj = desc.isInstantiable() ? desc.newInstance() : null;
-         } catch (Exception ex) {
-             throw new InvalidClassException(desc.forClass().getName(),
-                                             "unable to create instance", ex);
-         }
- 
-         passHandle = handles.assign(unshared ? unsharedMarker : obj);
+         // Assign the handle and initially set to null or the unsharedMarker
+         passHandle = handles.assign(unshared ? unsharedMarker : null);
          ClassNotFoundException resolveEx = desc.getResolveException();
          if (resolveEx != null) {
              handles.markException(passHandle, resolveEx);
          }
  
-         final boolean isRecord = desc.isRecord();
-         if (isRecord) {
-             assert obj == null;
-             obj = readRecord(desc);
-             if (!unshared)
-                 handles.setObject(passHandle, obj);
-         } else if (desc.isExternalizable()) {
-             readExternalData((Externalizable) obj, desc);
-         } else {
-             readSerialData(obj, desc);
-         }
+         try {
+             // Dispatch on the factory mode to read an object from the stream.
+             Object obj = switch (desc.factoryMode()) {
+                 case READ_OBJECT_DEFAULT -> readSerialDefaultObject(desc, unshared);
+                 case READ_OBJECT_CUSTOM -> readSerialCustomData(desc, unshared);
+                 case READ_RECORD -> readRecord(desc, unshared);
+                 case READ_EXTERNALIZABLE -> readExternalObject(desc, unshared);
+                 case READ_OBJECT_VALUE -> readObjectValue(desc, unshared);
+                 case READ_NO_LOCAL_CLASS -> readAbsentLocalClass(desc, unshared);
+                 case null -> throw new AssertionError("Unknown factoryMode for: " + desc.getName(),
+                         resolveEx);
+             };
  
-         handles.finish(passHandle);
+             handles.finish(passHandle);
  
-         if (obj != null &&
-             handles.lookupException(passHandle) == null &&
-             desc.hasReadResolveMethod())
-         {
-             Object rep = desc.invokeReadResolve(obj);
-             if (unshared && rep.getClass().isArray()) {
-                 rep = cloneArray(rep);
-             }
-             if (rep != obj) {
-                 // Filter the replacement object
-                 if (rep != null) {
-                     if (rep.getClass().isArray()) {
-                         filterCheck(rep.getClass(), Array.getLength(rep));
-                     } else {
-                         filterCheck(rep.getClass(), -1);
+             if (obj != null &&
+                 handles.lookupException(passHandle) == null &&
+                 desc.hasReadResolveMethod())
+             {
+                 Object rep = desc.invokeReadResolve(obj);
+                 if (unshared && rep.getClass().isArray()) {
+                     rep = cloneArray(rep);
+                 }
+                 if (rep != obj) {
+                     // Filter the replacement object
+                     if (rep != null) {
+                         if (rep.getClass().isArray()) {
+                             filterCheck(rep.getClass(), Array.getLength(rep));
+                         } else {
+                             filterCheck(rep.getClass(), -1);
+                         }
                      }
+                     handles.setObject(passHandle, obj = rep);
                  }
-                 handles.setObject(passHandle, obj = rep);
+             }
+ 
+             return obj;
+         } catch (UncheckedIOException uioe) {
+             // Consistent re-throw for nested UncheckedIOExceptions
+             throw uioe.getCause();
+         }
+     }
+ 
+     /**
+      * {@return a value class instance by invoking its constructor with field values read from the stream.
+      * The fields of the class in the stream are matched to the local fields and applied to
+      * the constructor.
+      * If the stream contains superclasses with serializable fields,
+      * an InvalidClassException is thrown with an incompatible class change message.
+      *
+      * @param desc the class descriptor read from the stream, the local class is a value class
+      * @param unshared if the object is not to be shared
+      * @throws InvalidClassException if the stream contains a superclass with serializable fields.
+      * @throws IOException if there are I/O errors while reading from the
+      *         underlying {@code InputStream}
+      */
+     private Object readObjectValue(ObjectStreamClass desc, boolean unshared) throws IOException {
+         final ObjectStreamClass localDesc = desc.getLocalDesc();
+         TRACE("readObjectValue: %s, local class: %s", desc.getName(), localDesc.getName());
+         // Check for un-expected fields in superclasses
+         List<ClassDataSlot> slots = desc.getClassDataLayout();
+         for (int i = 0; i < slots.size()-1; i++) {
+             ClassDataSlot slot = slots.get(i);
+             if (slot.hasData && slot.desc.getFields(false).length > 0) {
+                 throw new InvalidClassException("incompatible class change to value class: " +
+                         "stream class has non-empty super type: " + desc.getName());
              }
          }
+         // Read values for the value class fields
+         FieldValues fieldValues = new FieldValues(desc, true);
  
-         return obj;
+         // Get value object constructor adapted to take primitive value buffer and object array.
+         MethodHandle consMH = ConstructorSupport.deserializationValueCons(desc);
+         try {
+             Object obj = (Object) consMH.invokeExact(fieldValues.primValues, fieldValues.objValues);
+             if (!unshared)
+                 handles.setObject(passHandle, obj);
+             return obj;
+         } catch (Exception e) {
+             throw new InvalidObjectException(e.getMessage(), e);
+         } catch (Error e) {
+             throw e;
+         } catch (Throwable t) {
+             throw new InvalidObjectException("ReflectiveOperationException " +
+                     "during deserialization", t);
+         }
      }
  
      /**
-      * If obj is non-null, reads externalizable data by invoking readExternal()
+      * Creates a new object and invokes its readExternal method to read its contents.
+      *
+      * If the class is instantiable, read externalizable data by invoking readExternal()
       * method of obj; otherwise, attempts to skip over externalizable data.
       * Expects that passHandle is set to obj's handle before this method is
-      * called.
+      * called.  The new object is entered in the handle table immediately,
+      * allowing it to leak before it is completely read.
       */
-     private void readExternalData(Externalizable obj, ObjectStreamClass desc)
+     private Object readExternalObject(ObjectStreamClass desc, boolean unshared)
          throws IOException
      {
+         TRACE("readExternalObject: %s", desc.getName());
+ 
+         // For Externalizable objects,
+         // create the instance, publish the ref, and read the data
+         Externalizable obj = null;
+         try {
+             if (desc.isInstantiable()) {
+                 obj = (Externalizable) desc.newInstance();
+             }
+         } catch (Exception ex) {
+             throw new InvalidClassException(desc.getName(),
+                     "unable to create instance", ex);
+         }
+ 
+         if (!unshared)
+             handles.setObject(passHandle, obj);
+ 
          SerialCallbackContext oldContext = curContext;
          if (oldContext != null)
              oldContext.check();
          curContext = null;
          try {

@@ -2354,26 +2448,28 @@
           * we mimic the behavior of past serialization implementations and
           * blindly hope that the stream is in sync; if it isn't and additional
           * externalizable data remains in the stream, a subsequent read will
           * most likely throw a StreamCorruptedException.
           */
+         return obj;
      }
  
      /**
       * Reads and returns a record.
       * If an exception is marked for any of the fields, the dependency
       * mechanism marks the record as having an exception.
       * Null is returned from readRecord and later the exception is thrown at
       * the exit of {@link #readObject(Class)}.
-      **/
-     private Object readRecord(ObjectStreamClass desc) throws IOException {
-         ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
-         if (slots.length != 1) {
+      */
+     private Object readRecord(ObjectStreamClass desc, boolean unshared) throws IOException {
+         TRACE("invoking readRecord: %s", desc.getName());
+         List<ClassDataSlot> slots = desc.getClassDataLayout();
+         if (slots.size() != 1) {
              // skip any superclass stream field values
-             for (int i = 0; i < slots.length-1; i++) {
-                 if (slots[i].hasData) {
-                     new FieldValues(slots[i].desc, true);
+             for (int i = 0; i < slots.size()-1; i++) {
+                 if (slots.get(i).hasData) {
+                     new FieldValues(slots.get(i).desc, true);
                  }
              }
          }
  
          FieldValues fieldValues = new FieldValues(desc, true);

@@ -2383,14 +2479,17 @@
  
          // get canonical record constructor adapted to take two arguments:
          // - byte[] primValues
          // - Object[] objValues
          // and return Object
-         MethodHandle ctrMH = RecordSupport.deserializationCtr(desc);
+         MethodHandle ctrMH = ConstructorSupport.deserializationCtr(desc);
  
          try {
-             return (Object) ctrMH.invokeExact(fieldValues.primValues, fieldValues.objValues);
+             Object obj = (Object) ctrMH.invokeExact(fieldValues.primValues, fieldValues.objValues);
+             if (!unshared)
+                 handles.setObject(passHandle, obj);
+             return obj;
          } catch (Exception e) {
              throw new InvalidObjectException(e.getMessage(), e);
          } catch (Error e) {
              throw e;
          } catch (Throwable t) {

@@ -2398,118 +2497,211 @@
                                               "during deserialization", t);
          }
      }
  
      /**
-      * Reads (or attempts to skip, if obj is null or is tagged with a
+      * Construct an object from the stream for a class that has only default read object behaviors.
+      * For each object, the fields are read before any are assigned.
+      * The new instance is entered in the handle table if it is unshared,
+      * allowing it to escape before it is initialized.
+      * The `readObject` and `readObjectNoData` methods are not present and are not called.
+      *
+      * @param desc the class descriptor
+      * @param unshared true if the object should be shared
+      * @return the object constructed from the stream data
+      * @throws IOException if there are I/O errors while reading from the
+      *         underlying {@code InputStream}
+      * @throws InvalidClassException if the instance creation fails
+      */
+     private Object readSerialDefaultObject(ObjectStreamClass desc, boolean unshared)
+             throws IOException, InvalidClassException {
+         if (!desc.isInstantiable()) {
+             // No local class to create, read and discard
+             return readAbsentLocalClass(desc, unshared);
+         }
+         TRACE("readSerialDefaultObject: %s", desc.getName());
+         try {
+             final Object obj = desc.newInstance();
+             if (!unshared)
+                 handles.setObject(passHandle, obj);
+ 
+             // Best effort Failure Atomicity; slotValues will be non-null if field
+             // values can be set after reading all field data in the hierarchy.
+             List<FieldValues> slotValues = desc.getClassDataLayout().stream()
+                     .filter(s -> s.hasData)
+                     .map(s1 -> {
+                         var values = new FieldValues(s1.desc, true);
+                         finishBlockData(s1.desc);
+                         return values;
+                     })
+                     .toList();
+ 
+             if (handles.lookupException(passHandle) != null) {
+                 return null;    // some exception for a class, do not return the object
+             }
+ 
+             // Check that the types are assignable for all slots before assigning.
+             slotValues.forEach(v -> v.defaultCheckFieldValues(obj));
+             slotValues.forEach(v -> v.defaultSetFieldValues(obj));
+             return obj;
+         } catch (InstantiationException | InvocationTargetException ex) {
+             throw new InvalidClassException(desc.forClass().getName(),
+                     "unable to create instance", ex);
+         }
+     }
+ 
+ 
+     /**
+      * Reads (or attempts to skip, if not instantiatable or is tagged with a
       * ClassNotFoundException) instance data for each serializable class of
-      * object in stream, from superclass to subclass.  Expects that passHandle
-      * is set to obj's handle before this method is called.
+      * object in stream, from superclass to subclass.
+      * Expects that passHandle is set to current handle before this method is called.
       */
-     private void readSerialData(Object obj, ObjectStreamClass desc)
+     private Object readSerialCustomData(ObjectStreamClass desc, boolean unshared)
          throws IOException
      {
-         ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
-         // Best effort Failure Atomicity; slotValues will be non-null if field
-         // values can be set after reading all field data in the hierarchy.
-         // Field values can only be set after reading all data if there are no
-         // user observable methods in the hierarchy, readObject(NoData). The
-         // top most Serializable class in the hierarchy can be skipped.
-         FieldValues[] slotValues = null;
- 
-         boolean hasSpecialReadMethod = false;
-         for (int i = 1; i < slots.length; i++) {
-             ObjectStreamClass slotDesc = slots[i].desc;
-             if (slotDesc.hasReadObjectMethod()
-                   || slotDesc.hasReadObjectNoDataMethod()) {
-                 hasSpecialReadMethod = true;
-                 break;
-             }
+         if (!desc.isInstantiable()) {
+             // No local class to create, read and discard
+             return readAbsentLocalClass(desc, unshared);
          }
-         // No special read methods, can store values and defer setting.
-         if (!hasSpecialReadMethod)
-             slotValues = new FieldValues[slots.length];
  
-         for (int i = 0; i < slots.length; i++) {
-             ObjectStreamClass slotDesc = slots[i].desc;
- 
-             if (slots[i].hasData) {
-                 if (obj == null || handles.lookupException(passHandle) != null) {
-                     // Read fields of the current descriptor into a new FieldValues and discard
-                     new FieldValues(slotDesc, true);
-                 } else if (slotDesc.hasReadObjectMethod()) {
-                     SerialCallbackContext oldContext = curContext;
-                     if (oldContext != null)
-                         oldContext.check();
-                     try {
-                         curContext = new SerialCallbackContext(obj, slotDesc);
+         TRACE("readSerialCustomData: %s, ex: %s", desc.getName(), handles.lookupException(passHandle));
+         try {
+             Object obj = desc.newInstance();
+             if (!unshared)
+                 handles.setObject(passHandle, obj);
+             // Read data into each of the slots for the class
+             return readSerialCustomSlots(obj, desc.getClassDataLayout());
+         } catch (InstantiationException | InvocationTargetException ex) {
+             throw new InvalidClassException(desc.forClass().getName(),
+                     "unable to create instance", ex);
+         }
+     }
  
-                         bin.setBlockDataMode(true);
-                         slotDesc.invokeReadObject(obj, this);
-                     } catch (ClassNotFoundException ex) {
-                         /*
-                          * In most cases, the handle table has already
-                          * propagated a CNFException to passHandle at this
-                          * point; this mark call is included to address cases
-                          * where the custom readObject method has cons'ed and
-                          * thrown a new CNFException of its own.
-                          */
-                         handles.markException(passHandle, ex);
-                     } finally {
-                         curContext.setUsed();
-                         if (oldContext!= null)
-                             oldContext.check();
-                         curContext = oldContext;
-                     }
+     /**
+      * Reads from the stream using custom or default readObject methods appropriate.
+      * For each slot, either the custom readObject method or the default reader of fields
+      * is invoked. Unused slot specific custom data is discarded.
+      * This function is used by {@link #readSerialCustomData}.
+      *
+      * @param obj the object to assign the values to
+      * @param slots a list of slots to read from the stream
+      * @return the object being initialized
+      * @throws IOException if there are I/O errors while reading from the
+      *         underlying {@code InputStream}
+      */
+     private Object readSerialCustomSlots(Object obj, List<ClassDataSlot> slots) throws IOException {
+         TRACE("    readSerialCustomSlots: %s", slots);
  
-                     /*
-                      * defaultDataEnd may have been set indirectly by custom
-                      * readObject() method when calling defaultReadObject() or
-                      * readFields(); clear it to restore normal read behavior.
-                      */
-                     defaultDataEnd = false;
+         for (ClassDataSlot slot : slots) {
+             ObjectStreamClass slotDesc = slot.desc;
+             if (slot.hasData) {
+                 if (slotDesc.hasReadObjectMethod() &&
+                         handles.lookupException(passHandle) == null) {
+                     // Invoke slot custom readObject method
+                     readSlotViaReadObject(obj, slotDesc);
                  } else {
                      // Read fields of the current descriptor into a new FieldValues
                      FieldValues values = new FieldValues(slotDesc, true);
-                     if (slotValues != null) {
-                         slotValues[i] = values;
-                     } else if (obj != null) {
-                         if (handles.lookupException(passHandle) == null) {
-                             // passHandle NOT marked with an exception; set field values
-                             values.defaultCheckFieldValues(obj);
-                             values.defaultSetFieldValues(obj);
-                         }
+                     if (handles.lookupException(passHandle) == null) {
+                         // Set the instance fields if no previous exception
+                         values.defaultCheckFieldValues(obj);
+                         values.defaultSetFieldValues(obj);
                      }
-                 }
- 
-                 if (slotDesc.hasWriteObjectData()) {
-                     skipCustomData();
-                 } else {
-                     bin.setBlockDataMode(false);
+                     finishBlockData(slotDesc);
                  }
              } else {
-                 if (obj != null &&
-                     slotDesc.hasReadObjectNoDataMethod() &&
-                     handles.lookupException(passHandle) == null)
-                 {
+                 if (slotDesc.hasReadObjectNoDataMethod() &&
+                         handles.lookupException(passHandle) == null) {
                      slotDesc.invokeReadObjectNoData(obj);
                  }
              }
          }
+         return obj;
+     }
  
-         if (obj != null && slotValues != null && handles.lookupException(passHandle) == null) {
-             // passHandle NOT marked with an exception
-             // Check that the non-primitive types are assignable for all slots
-             // before assigning.
-             for (int i = 0; i < slots.length; i++) {
-                 if (slotValues[i] != null)
-                     slotValues[i].defaultCheckFieldValues(obj);
-             }
-             for (int i = 0; i < slots.length; i++) {
-                 if (slotValues[i] != null)
-                     slotValues[i].defaultSetFieldValues(obj);
+     /**
+      * Invoke the readObject method of the class to read and store the state from the stream.
+      *
+      * @param obj an instance of the class being created, only partially initialized.
+      * @param slotDesc the ObjectStreamDescriptor for the current class
+      * @throws IOException if there are I/O errors while reading from the
+      *         underlying {@code InputStream}
+      */
+     private void readSlotViaReadObject(Object obj, ObjectStreamClass slotDesc) throws IOException {
+         TRACE("readSlotViaReadObject: %s", slotDesc.getName());
+         assert obj != null : "readSlotViaReadObject called when obj == null";
+ 
+         SerialCallbackContext oldContext = curContext;
+         if (oldContext != null)
+             oldContext.check();
+         try {
+             curContext = new SerialCallbackContext(obj, slotDesc);
+ 
+             bin.setBlockDataMode(true);
+             slotDesc.invokeReadObject(obj, this);
+         } catch (ClassNotFoundException ex) {
+             /*
+              * In most cases, the handle table has already
+              * propagated a CNFException to passHandle at this
+              * point; this mark call is included to address cases
+              * where the custom readObject method has cons'ed and
+              * thrown a new CNFException of its own.
+              */
+             handles.markException(passHandle, ex);
+         } finally {
+             curContext.setUsed();
+             if (oldContext!= null)
+                 oldContext.check();
+             curContext = oldContext;
+         }
+ 
+         /*
+          * defaultDataEnd may have been set indirectly by custom
+          * readObject() method when calling defaultReadObject() or
+          * readFields(); clear it to restore normal read behavior.
+          */
+         defaultDataEnd = false;
+ 
+         finishBlockData(slotDesc);
+     }
+ 
+ 
+     /**
+      * Read and discard an entire object, leaving a null reference in the HandleTable.
+      * The descriptor of the class in the stream is used to read the fields from the stream.
+      * There is no instance in which to store the field values.
+      * Custom data following the fields of any slot is read and discarded.
+      * References to nested objects are read and retained in the
+      * handle table using the regular mechanism.
+      * Handles later in the stream may refer to the nested objects.
+      *
+      * @param desc the stream class descriptor
+      * @param unshared the unshared flag, ignored since no object is created
+      * @return null, no object is created
+      * @throws IOException if there are I/O errors while reading from the
+      *         underlying {@code InputStream}
+      */
+     private Object readAbsentLocalClass(ObjectStreamClass desc, boolean unshared)
+             throws IOException {
+         TRACE("readAbsentLocalClass: %s", desc.getName());
+         desc.getClassDataLayout().stream()
+                 .filter(s -> s.hasData)
+                 .forEach(s2 -> {new FieldValues(s2.desc, true); finishBlockData(s2.desc);});
+         return null;
+     }
+ 
+     // Finish handling of block data by skipping any remaining and setting BlockDataMode = false
+     private void finishBlockData(ObjectStreamClass slotDesc) throws UncheckedIOException {
+         try {
+             if (slotDesc.hasWriteObjectData()) {
+                 skipCustomData();
+             } else {
+                 bin.setBlockDataMode(false);
              }
+         } catch (IOException ioe) {
+             throw new UncheckedIOException(ioe);
          }
      }
  
      /**
       * Skips over all block data and objects until TC_ENDBLOCKDATA is

@@ -2601,36 +2793,42 @@
           * Creates FieldValues object for reading fields defined in given
           * class descriptor.
           * @param desc the ObjectStreamClass to read
           * @param recordDependencies if true, record the dependencies
           *                           from current PassHandle and the object's read.
+          * @throws UncheckedIOException if any IOException occurs
           */
-         FieldValues(ObjectStreamClass desc, boolean recordDependencies) throws IOException {
-             this.desc = desc;
+         FieldValues(ObjectStreamClass desc, boolean recordDependencies) throws UncheckedIOException {
+             try {
+                 this.desc = desc;
+                 TRACE("    reading FieldValues: %s", desc.getName());
+                 int primDataSize = desc.getPrimDataSize();
+                 primValues = (primDataSize > 0) ? new byte[primDataSize] : null;
+                 if (primDataSize > 0) {
+                     bin.readFully(primValues, 0, primDataSize, false);
+                 }
  
-             int primDataSize = desc.getPrimDataSize();
-             primValues = (primDataSize > 0) ? new byte[primDataSize] : null;
-             if (primDataSize > 0) {
-                 bin.readFully(primValues, 0, primDataSize, false);
-             }
  
-             int numObjFields = desc.getNumObjFields();
-             objValues = (numObjFields > 0) ? new Object[numObjFields] : null;
-             objHandles = (numObjFields > 0) ? new int[numObjFields] : null;
-             if (numObjFields > 0) {
-                 int objHandle = passHandle;
-                 ObjectStreamField[] fields = desc.getFields(false);
-                 int numPrimFields = fields.length - objValues.length;
-                 for (int i = 0; i < objValues.length; i++) {
-                     ObjectStreamField f = fields[numPrimFields + i];
-                     objValues[i] = readObject0(Object.class, f.isUnshared());
-                     objHandles[i] = passHandle;
-                     if (recordDependencies && f.getField() != null) {
-                         handles.markDependency(objHandle, passHandle);
+                 int numObjFields = desc.getNumObjFields();
+                 objValues = (numObjFields > 0) ? new Object[numObjFields] : null;
+                 objHandles = (numObjFields > 0) ? new int[numObjFields] : null;
+                 if (numObjFields > 0) {
+                     int objHandle = passHandle;
+                     ObjectStreamField[] fields = desc.getFields(false);
+                     int numPrimFields = fields.length - objValues.length;
+                     for (int i = 0; i < objValues.length; i++) {
+                         ObjectStreamField f = fields[numPrimFields + i];
+                         objValues[i] = readObject0(Object.class, f.isUnshared());
+                         objHandles[i] = passHandle;
+                         if (recordDependencies && f.getField() != null) {
+                             handles.markDependency(objHandle, passHandle);
+                         }
                      }
+                     passHandle = objHandle;
                  }
-                 passHandle = objHandle;
+             } catch (IOException ioe) {
+                 throw new UncheckedIOException(ioe);
              }
          }
  
          public ObjectStreamClass getObjectStreamClass() {
              return desc;
< prev index next >