< prev index next >

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

Print this page
@@ -1,7 +1,7 @@
  /*
-  * Copyright (c) 1996, 2023, Oracle and/or its affiliates. All rights reserved.
+  * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved.
   * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   *
   * This code is free software; you can redistribute it and/or modify it
   * under the terms of the GNU General Public License version 2 only, as
   * published by the Free Software Foundation.  Oracle designates this

@@ -28,10 +28,11 @@
  import java.lang.invoke.MethodHandle;
  import java.lang.invoke.MethodHandles;
  import java.lang.invoke.MethodType;
  import java.lang.reflect.Constructor;
  import java.lang.reflect.Field;
+ import java.lang.reflect.InaccessibleObjectException;
  import java.lang.reflect.InvocationTargetException;
  import java.lang.reflect.RecordComponent;
  import java.lang.reflect.UndeclaredThrowableException;
  import java.lang.reflect.Member;
  import java.lang.reflect.Method;

@@ -50,32 +51,38 @@
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.HashSet;
+ import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.concurrent.ConcurrentHashMap;
+ import java.util.stream.Stream;
  
+ import jdk.internal.MigratedValueClass;
  import jdk.internal.event.SerializationMisdeclarationEvent;
  import jdk.internal.misc.Unsafe;
  import jdk.internal.reflect.CallerSensitive;
  import jdk.internal.reflect.Reflection;
  import jdk.internal.reflect.ReflectionFactory;
  import jdk.internal.access.SharedSecrets;
  import jdk.internal.access.JavaSecurityAccess;
  import jdk.internal.util.ByteArray;
+ import jdk.internal.value.DeserializeConstructor;
  import sun.reflect.misc.ReflectUtil;
  
+ import static java.io.ObjectInputStream.TRACE;
+ 
  /**
   * Serialization's descriptor for classes.  It contains the name and
   * serialVersionUID of the class.  The ObjectStreamClass for a specific class
   * loaded in this Java VM can be found/created using the lookup method.
   *
   * <p>The algorithm to compute the SerialVersionUID is described in
   * <a href="{@docRoot}/../specs/serialization/class.html#stream-unique-identifiers">
-  *    <cite>Java Object Serialization Specification,</cite> Section 4.6, "Stream Unique Identifiers"</a>.
+  *    <cite>Java Object Serialization Specification</cite>, Section 4.6, "Stream Unique Identifiers"</a>.
   *
   * @spec serialization/index.html Java Object Serialization Specification
   * @author      Mike Warres
   * @author      Roger Riggs
   * @see ObjectStreamField

@@ -102,10 +109,75 @@
      @SuppressWarnings("removal")
      private static final ReflectionFactory reflFactory =
          AccessController.doPrivileged(
              new ReflectionFactory.GetReflectionFactoryAction());
  
+     /**
+      * The mode of deserialization for a class depending on its type and interfaces.
+      * The markers used are {@linkplain java.io.Serializable}, {@linkplain java.io.Externalizable},
+      * Class.isRecord(), Class.isValue(), constructors, and
+      * the presence of methods `readObject`, `writeObject`, `readObjectNoData`, `writeObject`.
+      * ObjectInputStream dispatches on the mode to construct objects from the stream.
+      */
+     enum DeserializationMode {
+         /**
+          * Construct an object from the stream for a class that has only default read object behaviors.
+          * All classes and superclasses use defaultReadObject; no custom readObject or readObjectNoData.
+          * The new instance is entered in the handle table if it is unshared,
+          * allowing it to escape before it is initialized.
+          * For each object, all the fields are read before any are assigned.
+          * The `readObject` and `readObjectNoData` methods are not present and are not called.
+          */
+         READ_OBJECT_DEFAULT,
+         /**
+          * 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.  The new object is entered in the handle table immediately,
+          * allowing it to leak before it is completely read.
+          */
+         READ_EXTERNALIZABLE,
+         /**
+          * Read all the record fields and invoke its canonical constructor.
+          * Construct the record using its canonical constructor.
+          * The new record is entered in the handle table only after the constructor returns.
+          */
+         READ_RECORD,
+         /**
+          * Fully custom read from the stream to create an instance.
+          * If the class is not instantiatable or is tagged with ClassNotFoundException
+          * the data in the stream for the class is read and discarded. {@link #READ_NO_LOCAL_CLASS}
+          * The instance is created and set in the handle table, allowing it to leak before it is initialized.
+          * For each serializable class in the stream, from superclass to subclass the
+          * stream values are read by the `readObject` method, if present, or defaultReadObject.
+          * Custom inline data is discarded if not consumed by the class `readObject` method.
+          */
+         READ_OBJECT_CUSTOM,
+         /**
+          * Construct an object by reading the values of all fields and
+          * invoking a constructor or static factory method.
+          * The constructor or static factory method is selected by matching its parameters with the
+          * sequence of field types of the serializable fields of the local class and superclasses.
+          * Invoke the constructor with all the values from the stream, inserting
+          * defaults and dropping extra values as necessary.
+          * This is very similar to the reading of records, except for the identification of
+          * the constructor or static factory.
+          */
+         READ_OBJECT_VALUE,
+         /**
+          * 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.
+          */
+         READ_NO_LOCAL_CLASS,
+     }
+ 
      private static class Caches {
          /** cache mapping local classes -> descriptors */
          static final ClassCache<ObjectStreamClass> localDescs =
              new ClassCache<>() {
                  @Override

@@ -135,10 +207,14 @@
      private boolean isProxy;
      /** true if represents enum type */
      private boolean isEnum;
      /** true if represents record type */
      private boolean isRecord;
+     /** true if represents a value class */
+     private boolean isValue;
+     /** The DeserializationMode for this class. */
+     private DeserializationMode factoryMode;
      /** true if represented class implements Serializable */
      private boolean serializable;
      /** true if represented class implements Externalizable */
      private boolean externalizable;
      /** true if desc has data written by class-defined writeObject method */

@@ -192,11 +268,11 @@
      /** number of non-primitive fields */
      private int numObjFields;
      /** reflector for setting/getting serializable field values */
      private FieldReflector fieldRefl;
      /** data layout of serialized objects described by this class desc */
-     private volatile ClassDataSlot[] dataLayout;
+     private volatile List<ClassDataSlot> dataLayout;
  
      /** serialization-appropriate constructor, or null if none */
      private Constructor<?> cons;
      /** record canonical constructor (shared among OSCs for same class), or null */
      private MethodHandle canonicalCtr;

@@ -374,10 +450,11 @@
          this.cl = cl;
          name = cl.getName();
          isProxy = Proxy.isProxyClass(cl);
          isEnum = Enum.class.isAssignableFrom(cl);
          isRecord = cl.isRecord();
+         isValue = cl.isValue();
          serializable = Serializable.class.isAssignableFrom(cl);
          externalizable = Externalizable.class.isAssignableFrom(cl);
  
          Class<?> superCl = cl.getSuperclass();
          superDesc = (superCl != null) ? lookup(superCl, false) : null;

@@ -405,14 +482,38 @@
                              new ExceptionInfo(e.classname, e.getMessage());
                          fields = NO_FIELDS;
                      }
  
                      if (isRecord) {
+                         factoryMode = DeserializationMode.READ_RECORD;
                          canonicalCtr = canonicalRecordCtr(cl);
                          deserializationCtrs = new DeserializationConstructorsCache();
                      } else if (externalizable) {
-                         cons = getExternalizableConstructor(cl);
+                         factoryMode = DeserializationMode.READ_EXTERNALIZABLE;
+                         if (cl.isIdentity()) {
+                             cons = getExternalizableConstructor(cl);
+                         } else {
+                             serializeEx = deserializeEx = new ExceptionInfo(cl.getName(),
+                                     "Externalizable not valid for value class");
+                         }
+                     } else if (cl.isValue()) {
+                         factoryMode = DeserializationMode.READ_OBJECT_VALUE;
+                         if (!cl.isAnnotationPresent(MigratedValueClass.class)) {
+                             serializeEx = deserializeEx = new ExceptionInfo(cl.getName(),
+                                     "Value class serialization is only supported with `writeReplace`");
+                         } else if (Modifier.isAbstract(cl.getModifiers())) {
+                             serializeEx = deserializeEx = new ExceptionInfo(cl.getName(),
+                                     "value class is abstract");
+                         } else {
+                             // Value classes should have constructor(s) annotated with {@link DeserializeConstructor}
+                             canonicalCtr = getDeserializingValueCons(cl, fields);
+                             deserializationCtrs = new DeserializationConstructorsCache();                            factoryMode = DeserializationMode.READ_OBJECT_VALUE;
+                             if (canonicalCtr == null) {
+                                 serializeEx = deserializeEx = new ExceptionInfo(cl.getName(),
+                                         "no constructor or factory found for migrated value class");
+                             }
+                         }
                      } else {
                          cons = getSerializableConstructor(cl);
                          writeObjectMethod = getPrivateMethod(cl, "writeObject",
                              new Class<?>[] { ObjectOutputStream.class },
                              Void.TYPE);

@@ -420,10 +521,14 @@
                              new Class<?>[] { ObjectInputStream.class },
                              Void.TYPE);
                          readObjectNoDataMethod = getPrivateMethod(
                              cl, "readObjectNoData", null, Void.TYPE);
                          hasWriteObjectData = (writeObjectMethod != null);
+                         factoryMode = ((superDesc == null || superDesc.factoryMode() == DeserializationMode.READ_OBJECT_DEFAULT)
+                                 && readObjectMethod == null && readObjectNoDataMethod == null)
+                                 ? DeserializationMode.READ_OBJECT_DEFAULT
+                                 : DeserializationMode.READ_OBJECT_CUSTOM;
                      }
                      domains = getProtectionDomains(cons, cl);
                      writeReplaceMethod = getInheritableMethod(
                          cl, "writeReplace", null, Object.class);
                      readResolveMethod = getInheritableMethod(

@@ -444,11 +549,11 @@
          }
  
          if (deserializeEx == null) {
              if (isEnum) {
                  deserializeEx = new ExceptionInfo(name, "enum type");
-             } else if (cons == null && !isRecord) {
+             } else if (cons == null && !(isRecord | isValue)) {
                  deserializeEx = new ExceptionInfo(name, "no valid constructor");
              }
          }
          if (isRecord && canonicalCtr == null) {
              deserializeEx = new ExceptionInfo(name, "record canonical constructor not found");

@@ -564,10 +669,13 @@
              writeReplaceMethod = localDesc.writeReplaceMethod;
              readResolveMethod = localDesc.readResolveMethod;
              deserializeEx = localDesc.deserializeEx;
              domains = localDesc.domains;
              cons = localDesc.cons;
+             factoryMode = localDesc.factoryMode;
+         } else {
+             factoryMode = DeserializationMode.READ_OBJECT_DEFAULT;
          }
          fieldRefl = getReflector(fields, localDesc);
          initialized = true;
      }
  

@@ -642,10 +750,11 @@
          numObjFields = model.numObjFields;
  
          if (osc != null) {
              localDesc = osc;
              isRecord = localDesc.isRecord;
+             isValue = localDesc.isValue;
              // canonical record constructor is shared
              canonicalCtr = localDesc.canonicalCtr;
              // cache of deserialization constructors is shared
              deserializationCtrs = localDesc.deserializationCtrs;
              writeObjectMethod = localDesc.writeObjectMethod;

@@ -657,10 +766,16 @@
                  deserializeEx = localDesc.deserializeEx;
              }
              domains = localDesc.domains;
              assert cl.isRecord() ? localDesc.cons == null : true;
              cons = localDesc.cons;
+             factoryMode = localDesc.factoryMode;
+         } else {
+             // No local class, read data using only the schema from the stream
+             factoryMode = (externalizable)
+                     ? DeserializationMode.READ_EXTERNALIZABLE
+                     : DeserializationMode.READ_NO_LOCAL_CLASS;
          }
  
          fieldRefl = getReflector(fields, localDesc);
          // reassign to matched fields so as to reflect local unshared settings
          fields = fieldRefl.getFields();

@@ -712,11 +827,11 @@
              char tcode = (char) in.readByte();
              String fname = in.readUTF();
              String signature = ((tcode == 'L') || (tcode == '[')) ?
                  in.readTypeString() : String.valueOf(tcode);
              try {
-                 fields[i] = new ObjectStreamField(fname, signature, false);
+                 fields[i] = new ObjectStreamField(fname, signature, false, -1);
              } catch (RuntimeException e) {
                  throw new InvalidClassException(name,
                                                  "invalid descriptor for field " +
                                                  fname, e);
              }

@@ -923,10 +1038,26 @@
      boolean isSerializable() {
          requireInitialized();
          return serializable;
      }
  
+     /**
+      * {@return {code true} if the class is a value class, {@code false} otherwise}
+      */
+     boolean isValue() {
+         requireInitialized();
+         return isValue;
+     }
+ 
+     /**
+      * {@return the factory mode for deserialization}
+      */
+     DeserializationMode factoryMode() {
+         requireInitialized();
+         return factoryMode;
+     }
+ 
      /**
       * Returns true if class descriptor represents externalizable class that
       * has written its data in 1.2 (block data) format, false otherwise.
       */
      boolean hasBlockExternalData() {

@@ -945,17 +1076,19 @@
      }
  
      /**
       * Returns true if represented class is serializable/externalizable and can
       * be instantiated by the serialization runtime--i.e., if it is
-      * externalizable and defines a public no-arg constructor, or if it is
+      * externalizable and defines a public no-arg constructor, if it is
       * non-externalizable and its first non-serializable superclass defines an
-      * accessible no-arg constructor.  Otherwise, returns false.
+      * accessible no-arg constructor, or if the class is a value class with a @DeserializeConstructor
+      * constructor or static factory.
+      * Otherwise, returns false.
       */
      boolean isInstantiable() {
          requireInitialized();
-         return (cons != null);
+         return (cons != null || (isValue() && canonicalCtr != null));
      }
  
      /**
       * Returns true if represented class is serializable (but not
       * externalizable) and defines a conformant writeObject method.  Otherwise,

@@ -1237,27 +1370,22 @@
              this.hasData = hasData;
          }
      }
  
      /**
-      * Returns array of ClassDataSlot instances representing the data layout
+      * Returns a List of ClassDataSlot instances representing the data layout
       * (including superclass data) for serialized objects described by this
       * class descriptor.  ClassDataSlots are ordered by inheritance with those
       * containing "higher" superclasses appearing first.  The final
       * ClassDataSlot contains a reference to this descriptor.
       */
-     ClassDataSlot[] getClassDataLayout() throws InvalidClassException {
+     List<ClassDataSlot> getClassDataLayout() throws InvalidClassException {
          // REMIND: synchronize instead of relying on volatile?
-         if (dataLayout == null) {
-             dataLayout = getClassDataLayout0();
-         }
-         return dataLayout;
-     }
+         List<ClassDataSlot> layout = dataLayout;
+         if (layout != null)
+             return layout;
  
-     private ClassDataSlot[] getClassDataLayout0()
-         throws InvalidClassException
-     {
          ArrayList<ClassDataSlot> slots = new ArrayList<>();
          Class<?> start = cl, end = cl;
  
          // locate closest non-serializable superclass
          while (end != null && Serializable.class.isAssignableFrom(end)) {

@@ -1302,11 +1430,12 @@
                  ObjectStreamClass.lookup(c, true), false));
          }
  
          // order slots from superclass -> subclass
          Collections.reverse(slots);
-         return slots.toArray(new ClassDataSlot[slots.size()]);
+         dataLayout = slots;
+         return slots;
      }
  
      /**
       * Returns aggregate size (in bytes) of marshalled primitive field values
       * for represented class.

@@ -1430,10 +1559,70 @@
              desc.initNonProxy(this, cl, null, superDesc);
          }
          return desc;
      }
  
+     /**
+      * Return a method handle for the static method or constructor(s) that matches the
+      * serializable fields and annotated with {@link DeserializeConstructor}.
+      * The descriptor for the class is still being initialized, so is passed the fields needed.
+      * @param clazz The class to query
+      * @param fields the serializable fields of the class
+      * @return a MethodHandle, null if none found
+      */
+     @SuppressWarnings("unchecked")
+     private static MethodHandle getDeserializingValueCons(Class<?> clazz,
+                                                           ObjectStreamField[] fields) {
+         // Search for annotated static factory in methods or constructors
+         MethodHandles.Lookup lookup = MethodHandles.lookup();
+         MethodHandle mh = Stream.concat(
+                 Arrays.stream(clazz.getDeclaredMethods()).filter(m -> Modifier.isStatic(m.getModifiers())),
+                 Arrays.stream(clazz.getDeclaredConstructors()))
+                 .filter(m -> m.isAnnotationPresent(DeserializeConstructor.class))
+                 .map(m -> {
+                     try {
+                         m.setAccessible(true);
+                         return (m instanceof Constructor<?> cons)
+                                 ? lookup.unreflectConstructor(cons)
+                                 : lookup.unreflect(((Method) m));
+                     } catch (IllegalAccessException iae) {
+                         throw new InternalError(iae);   // should not occur after setAccessible
+                     }})
+                 .filter(m -> matchFactoryParamTypes(clazz, m, fields))
+                 .findFirst().orElse(null);
+         TRACE("DeserializeConstructor for %s, mh: %s", clazz,  mh);
+         return mh;
+     }
+ 
+     /**
+      * Check that the parameters of the factory method match the fields of this class.
+      *
+      * @param mh a MethodHandle for a constructor or factory
+      * @return true if all fields match the parameters, false if not
+      */
+     private static boolean matchFactoryParamTypes(Class<?> clazz,
+                                                        MethodHandle mh,
+                                                        ObjectStreamField[] fields) {
+         TRACE("  matchFactoryParams checking class: %s, mh: %s", clazz, mh);
+         var params = mh.type().parameterList();
+         if (params.size() != fields.length) {
+             TRACE("   matchFactoryParams %s, arg count mismatch %d params != %d fields",
+                     clazz, params.size(), fields.length);
+             return false;    // Mismatch in count of fields and parameters
+         }
+         for (ObjectStreamField field : fields) {
+             int argIndex = field.getArgIndex();
+             final Class<?> paramtype = params.get(argIndex);
+             if (!field.getType().equals(paramtype)) {
+                 TRACE("   matchFactoryParams %s: argIndex: %d type mismatch field: %s != param: %s",
+                         clazz, argIndex, field.getType(), paramtype);
+                 return false;
+             }
+         }
+         return true;
+     }
+ 
      /**
       * Returns public no-arg constructor of given class, or null if none found.
       * Access checks are disabled on the returned constructor (if any), since
       * the defining class may still be non-public.
       */

@@ -1441,11 +1630,11 @@
          try {
              Constructor<?> cons = cl.getDeclaredConstructor((Class<?>[]) null);
              cons.setAccessible(true);
              return ((cons.getModifiers() & Modifier.PUBLIC) != 0) ?
                  cons : null;
-         } catch (NoSuchMethodException ex) {
+         } catch (NoSuchMethodException | InaccessibleObjectException ex) {
              return null;
          }
      }
  
      /**

@@ -1676,18 +1865,16 @@
              try {
                  Field f = cl.getDeclaredField(fname);
                  if ((f.getType() == spf.getType()) &&
                      ((f.getModifiers() & Modifier.STATIC) == 0))
                  {
-                     boundFields[i] =
-                         new ObjectStreamField(f, spf.isUnshared(), true);
+                     boundFields[i] = new ObjectStreamField(f, spf.isUnshared(), true, i);
                  }
              } catch (NoSuchFieldException ex) {
              }
              if (boundFields[i] == null) {
-                 boundFields[i] = new ObjectStreamField(
-                     fname, spf.getType(), spf.isUnshared());
+                 boundFields[i] = new ObjectStreamField(fname, spf.getType(), spf.isUnshared(), i);
              }
          }
          return boundFields;
      }
  

@@ -1700,13 +1887,13 @@
      private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
          Field[] clFields = cl.getDeclaredFields();
          ArrayList<ObjectStreamField> list = new ArrayList<>();
          int mask = Modifier.STATIC | Modifier.TRANSIENT;
  
-         for (int i = 0; i < clFields.length; i++) {
+         for (int i = 0, argIndex = 0; i < clFields.length; i++) {
              if ((clFields[i].getModifiers() & mask) == 0) {
-                 list.add(new ObjectStreamField(clFields[i], false, true));
+                 list.add(new ObjectStreamField(clFields[i], false, true, argIndex++));
              }
          }
          int size = list.size();
          return (size == 0) ? NO_FIELDS :
              list.toArray(new ObjectStreamField[size]);

@@ -2054,12 +2241,16 @@
              /* assuming checkDefaultSerialize() has been called on the class
               * descriptor this FieldReflector was obtained from, no field keys
               * in array should be equal to Unsafe.INVALID_FIELD_OFFSET.
               */
              for (int i = numPrimFields; i < fields.length; i++) {
+                 Field f = fields[i].getField();
                  vals[offsets[i]] = switch (typeCodes[i]) {
-                     case 'L', '[' -> UNSAFE.getReference(obj, readKeys[i]);
+                     case 'L', '[' ->
+                             UNSAFE.isFlatField(f)
+                                     ? UNSAFE.getValue(obj, readKeys[i], f.getType())
+                                     : UNSAFE.getReference(obj, readKeys[i]);
                      default       -> throw new InternalError();
                  };
              }
          }
  

@@ -2082,35 +2273,40 @@
          void setObjFieldValues(Object obj, Object[] vals) {
              setObjFieldValues(obj, vals, false);
          }
  
          private void setObjFieldValues(Object obj, Object[] vals, boolean dryRun) {
-             if (obj == null) {
+             if (obj == null && !dryRun) {
                  throw new NullPointerException();
              }
              for (int i = numPrimFields; i < fields.length; i++) {
                  long key = writeKeys[i];
                  if (key == Unsafe.INVALID_FIELD_OFFSET) {
                      continue;           // discard value
                  }
                  switch (typeCodes[i]) {
                      case 'L', '[' -> {
+                         Field f = fields[i].getField();
                          Object val = vals[offsets[i]];
                          if (val != null &&
                              !types[i - numPrimFields].isInstance(val))
                          {
-                             Field f = fields[i].getField();
                              throw new ClassCastException(
                                  "cannot assign instance of " +
                                  val.getClass().getName() + " to field " +
                                  f.getDeclaringClass().getName() + "." +
                                  f.getName() + " of type " +
                                  f.getType().getName() + " in instance of " +
                                  obj.getClass().getName());
                          }
-                         if (!dryRun)
-                             UNSAFE.putReference(obj, key, val);
+                         if (!dryRun) {
+                             if (UNSAFE.isFlatField(f)) {
+                                 UNSAFE.putValue(obj, key, f.getType(), val);
+                             } else {
+                                 UNSAFE.putReference(obj, key, val);
+                             }
+                         }
                      }
                      default -> throw new InternalError();
                  }
              }
          }

@@ -2218,20 +2414,20 @@
                          throw new InvalidClassException(localDesc.name,
                              "incompatible types for field " + f.getName());
                      }
                      if (lf.getField() != null) {
                          m = new ObjectStreamField(
-                             lf.getField(), lf.isUnshared(), false);
+                             lf.getField(), lf.isUnshared(), true, lf.getArgIndex()); // Don't hide type
                      } else {
                          m = new ObjectStreamField(
-                             lf.getName(), lf.getSignature(), lf.isUnshared());
+                             lf.getName(), lf.getSignature(), lf.isUnshared(), lf.getArgIndex());
                      }
                  }
              }
              if (m == null) {
                  m = new ObjectStreamField(
-                     f.getName(), f.getSignature(), false);
+                     f.getName(), f.getSignature(), false, -1);
              }
              m.setOffset(f.getOffset());
              matches[i] = m;
          }
          return matches;

@@ -2349,11 +2545,11 @@
              }
          }
      }
  
      /** Record specific support for retrieving and binding stream field values. */
-     static final class RecordSupport {
+     static final class ConstructorSupport {
          /**
           * Returns canonical record constructor adapted to take two arguments:
           * {@code (byte[] primValues, Object[] objValues)}
           * and return
           * {@code Object}

@@ -2406,10 +2602,69 @@
              // store it into cache and return the 1st value stored
              return desc.deserializationCtr =
                  desc.deserializationCtrs.putIfAbsentAndGet(desc.getFields(false), mh);
          }
  
+         /**
+          * Returns value object constructor adapted to take two arguments:
+          * {@code (byte[] primValues, Object[] objValues)} and return {@code Object}
+          */
+         static MethodHandle deserializationValueCons(ObjectStreamClass desc) {
+             // check the cached value 1st
+             MethodHandle mh = desc.deserializationCtr;
+             if (mh != null) return mh;
+             mh = desc.deserializationCtrs.get(desc.getFields(false));
+             if (mh != null) return desc.deserializationCtr = mh;
+ 
+             // retrieve the selected constructor
+             // (T1, T2, ..., Tn):TR
+             ObjectStreamClass localDesc = desc.localDesc;
+             mh = localDesc.canonicalCtr;
+             MethodType mt = mh.type();
+ 
+             // change return type to Object
+             // (T1, T2, ..., Tn):TR -> (T1, T2, ..., Tn):Object
+             mh = mh.asType(mh.type().changeReturnType(Object.class));
+ 
+             // drop last 2 arguments representing primValues and objValues arrays
+             // (T1, T2, ..., Tn):Object -> (T1, T2, ..., Tn, byte[], Object[]):Object
+             mh = MethodHandles.dropArguments(mh, mh.type().parameterCount(), byte[].class, Object[].class);
+ 
+             Class<?>[] params = mt.parameterArray();
+             for (int i = params.length-1; i >= 0; i--) {
+                 // Get the name from the local descriptor matching the argIndex
+                 var field = getFieldForArgIndex(localDesc, i);
+                 String name = (field == null) ? "" : field.getName();   // empty string to supply default
+                 Class<?> type = params[i];
+                 // obtain stream field extractor that extracts argument at
+                 // position i (Ti+1) from primValues and objValues arrays
+                 // (byte[], Object[]):Ti+1
+                 MethodHandle combiner = streamFieldExtractor(name, type, desc);
+                 // fold byte[] privValues and Object[] objValues into argument at position i (Ti+1)
+                 // (..., Ti, Ti+1, byte[], Object[]):Object -> (..., Ti, byte[], Object[]):Object
+                 mh = MethodHandles.foldArguments(mh, i, combiner);
+             }
+             // what we are left with is a MethodHandle taking just the primValues
+             // and objValues arrays and returning the constructed instance
+             // (byte[], Object[]):Object
+ 
+             // store it into cache and return the 1st value stored
+             return desc.deserializationCtr =
+                 desc.deserializationCtrs.putIfAbsentAndGet(desc.getFields(false), mh);
+         }
+ 
+         // Find the ObjectStreamField for the argument index, otherwise null
+         private static ObjectStreamField getFieldForArgIndex(ObjectStreamClass desc, int argIndex) {
+             for (var field : desc.fields) {
+                 if (field.getArgIndex() == argIndex)
+                     return field;
+             }
+             TRACE("field for ArgIndex is null: %s, index: %d, fields: %s",
+                     desc, argIndex, Arrays.toString(desc.fields));
+             return null;
+         }
+ 
          /** Returns the number of primitive fields for the given descriptor. */
          private static int numberPrimValues(ObjectStreamClass desc) {
              ObjectStreamField[] fields = desc.getFields();
              int primValueCount = 0;
              for (int i = 0; i < fields.length; i++) {
< prev index next >