8225054: Compiler implementation for records

8225052: javax.lang.model support for records
8225053: Preview APIs support for records
8225055: Javadoc for records
8226314: com.sun.source support for records
8227113: Specification for java.lang.Record
8233526: JVM support for records

Implement records in the compiler and the JVM, including serialization, reflection and APIs support

Co-authored-by: Brian Goetz <brian.goetz@oracle.com>
Co-authored-by: Maurizio Cimadamore <maurizio.cimadamore@oracle.com>
Co-authored-by: Harold Seigel <harold.seigel@oracle.com>
Co-authored-by: Joe Darcy <joe.darcy@oracle.com>
Co-authored-by: Jonathan Gibbons <jonathan.gibbons@oracle.com>
Co-authored-by: Chris Hegarty <chris.hegarty@oracle.com>
Co-authored-by: Jan Lahoda <jan.lahoda@oracle.com>
Reviewed-by: mcimadamore, briangoetz, alanb, darcy, chegar, jrose, jlahoda, coleenp, dholmes, lfoltan, mchung, sadayapalam, hannesw, sspitsyn
This commit is contained in:
Vicente Romero 2019-12-04 15:57:39 -05:00
parent 0a375cfa2d
commit 827e5e3226
351 changed files with 24958 additions and 6395 deletions

View file

@ -26,7 +26,9 @@
package java.io;
import java.io.ObjectStreamClass.WeakClassKey;
import java.io.ObjectStreamClass.RecordSupport;
import java.lang.System.Logger;
import java.lang.invoke.MethodHandle;
import java.lang.ref.ReferenceQueue;
import java.lang.reflect.Array;
import java.lang.reflect.Modifier;
@ -218,6 +220,39 @@ import sun.reflect.misc.ReflectUtil;
* Similarly, any serialPersistentFields or serialVersionUID field declarations
* are also ignored--all enum types have a fixed serialVersionUID of 0L.
*
* @implSpec
* <a id="record-serialization"></a>
* 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 <i>canonical</i> 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
* <a href="{@docRoot}/../specs/serialization/serial-arch.html#cyclic-references">
* [Section 1.14, "Circular References"</a> 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.
*
* @author Mike Warres
* @author Roger Riggs
* @see java.io.DataInput
@ -2047,6 +2082,11 @@ public class ObjectInputStream
return result;
}
@SuppressWarnings("preview")
private static boolean isRecord(Class<?> cls) {
return cls.isRecord();
}
/**
* Reads and returns "ordinary" (i.e., not a String, Class,
* ObjectStreamClass, array, or enum constant) object, or null if object's
@ -2085,7 +2125,12 @@ public class ObjectInputStream
handles.markException(passHandle, resolveEx);
}
if (desc.isExternalizable()) {
final boolean isRecord = cl != null && isRecord(cl) ? true : false;
if (isRecord) {
assert obj == null;
obj = readRecord(desc);
handles.setObject(passHandle, obj);
} else if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
@ -2171,6 +2216,43 @@ public class ObjectInputStream
*/
}
/** Reads a record. */
private Object readRecord(ObjectStreamClass desc) throws IOException {
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
if (slots.length != 1) {
// skip any superclass stream field values
for (int i = 0; i < slots.length-1; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
if (slots[i].hasData) {
defaultReadFields(null, slotDesc);
}
}
}
FieldValues fieldValues = defaultReadFields(null, desc);
// retrieve the canonical constructor
MethodHandle ctrMH = desc.getRecordConstructor();
// bind the stream field values
ctrMH = RecordSupport.bindCtrValues(ctrMH, desc, fieldValues);
try {
return ctrMH.invoke();
} catch (Exception e) {
InvalidObjectException ioe = new InvalidObjectException(e.getMessage());
ioe.initCause(e);
throw ioe;
} catch (Error e) {
throw e;
} catch (Throwable t) {
ObjectStreamException ose = new InvalidObjectException(
"ReflectiveOperationException during deserialization");
ose.initCause(t);
throw ose;
}
}
/**
* Reads (or attempts to skip, if obj is null or is tagged with a
* ClassNotFoundException) instance data for each serializable class of
@ -2317,7 +2399,7 @@ public class ObjectInputStream
}
}
private class FieldValues {
/*package-private*/ class FieldValues {
final byte[] primValues;
final Object[] objValues;

View file

@ -150,6 +150,10 @@ import sun.reflect.misc.ReflectUtil;
* defaultWriteObject and writeFields initially terminate any existing
* block-data record.
*
* @implSpec
* Records are serialized differently than ordinary serializable or externalizable
* objects, see <a href="ObjectInputStream.html#record-serialization">record serialization</a>.
*
* @author Mike Warres
* @author Roger Riggs
* @see java.io.DataOutput
@ -1431,7 +1435,10 @@ public class ObjectOutputStream
bout.writeByte(TC_OBJECT);
writeClassDesc(desc, false);
handles.assign(unshared ? null : obj);
if (desc.isExternalizable() && !desc.isProxy()) {
if (desc.isRecord()) {
writeRecordData(obj, desc);
} else if (desc.isExternalizable() && !desc.isProxy()) {
writeExternalData((Externalizable) obj);
} else {
writeSerialData(obj, desc);
@ -1475,6 +1482,21 @@ public class ObjectOutputStream
curPut = oldPut;
}
/** Writes the record component values for the given record object. */
@SuppressWarnings("preview")
private void writeRecordData(Object obj, ObjectStreamClass desc)
throws IOException
{
assert obj.getClass().isRecord();
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
if (slots.length != 1) {
throw new InvalidClassException(
"expected a single record slot length, but found: " + slots.length);
}
defaultWriteFields(obj, desc); // #### seems unnecessary to use the accessors
}
/**
* Writes instance data for each serializable class of given object, from
* superclass to subclass.

View file

@ -25,6 +25,8 @@
package java.io;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
@ -32,6 +34,7 @@ import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
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;
@ -44,6 +47,8 @@ import java.security.NoSuchAlgorithmException;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
@ -123,6 +128,8 @@ public class ObjectStreamClass implements Serializable {
private boolean isProxy;
/** true if represents enum type */
private boolean isEnum;
/** true if represents record type */
private boolean isRecord;
/** true if represented class implements Serializable */
private boolean serializable;
/** true if represented class implements Externalizable */
@ -184,6 +191,8 @@ public class ObjectStreamClass implements Serializable {
/** serialization-appropriate constructor, or null if none */
private Constructor<?> cons;
/** record canonical constructor, or null */
private MethodHandle canonicalCtr;
/** protection domains that need to be checked when calling the constructor */
private ProtectionDomain[] domains;
@ -261,6 +270,9 @@ public class ObjectStreamClass implements Serializable {
public long getSerialVersionUID() {
// REMIND: synchronize instead of relying on volatile?
if (suid == null) {
if (isRecord)
return 0L;
suid = AccessController.doPrivileged(
new PrivilegedAction<Long>() {
public Long run() {
@ -467,6 +479,11 @@ public class ObjectStreamClass implements Serializable {
}
}
@SuppressWarnings("preview")
private static boolean isRecord(Class<?> cls) {
return cls.isRecord();
}
/**
* Creates local class descriptor representing given class.
*/
@ -475,6 +492,7 @@ public class ObjectStreamClass implements Serializable {
name = cl.getName();
isProxy = Proxy.isProxyClass(cl);
isEnum = Enum.class.isAssignableFrom(cl);
isRecord = isRecord(cl);
serializable = Serializable.class.isAssignableFrom(cl);
externalizable = Externalizable.class.isAssignableFrom(cl);
@ -505,7 +523,9 @@ public class ObjectStreamClass implements Serializable {
fields = NO_FIELDS;
}
if (externalizable) {
if (isRecord) {
canonicalCtr = canonicalRecordCtr(cl);
} else if (externalizable) {
cons = getExternalizableConstructor(cl);
} else {
cons = getSerializableConstructor(cl);
@ -542,14 +562,18 @@ public class ObjectStreamClass implements Serializable {
if (deserializeEx == null) {
if (isEnum) {
deserializeEx = new ExceptionInfo(name, "enum type");
} else if (cons == null) {
} else if (cons == null && !isRecord) {
deserializeEx = new ExceptionInfo(name, "no valid constructor");
}
}
for (int i = 0; i < fields.length; i++) {
if (fields[i].getField() == null) {
defaultSerializeEx = new ExceptionInfo(
name, "unmatched serializable field(s) declared");
if (isRecord && canonicalCtr == null) {
deserializeEx = new ExceptionInfo(name, "record canonical constructor not found");
} else {
for (int i = 0; i < fields.length; i++) {
if (fields[i].getField() == null) {
defaultSerializeEx = new ExceptionInfo(
name, "unmatched serializable field(s) declared");
}
}
}
initialized = true;
@ -682,7 +706,7 @@ public class ObjectStreamClass implements Serializable {
}
if (model.serializable == osc.serializable &&
!cl.isArray() &&
!cl.isArray() && !isRecord(cl) &&
suid != osc.getSerialVersionUID()) {
throw new InvalidClassException(osc.name,
"local class incompatible: " +
@ -714,6 +738,10 @@ public class ObjectStreamClass implements Serializable {
}
this.cl = cl;
if (cl != null) {
this.isRecord = isRecord(cl);
this.canonicalCtr = osc.canonicalCtr;
}
this.resolveEx = resolveEx;
this.superDesc = superDesc;
name = model.name;
@ -739,12 +767,14 @@ public class ObjectStreamClass implements Serializable {
deserializeEx = localDesc.deserializeEx;
}
domains = localDesc.domains;
assert isRecord(cl) ? localDesc.cons == null : true;
cons = localDesc.cons;
}
fieldRefl = getReflector(fields, localDesc);
// reassign to matched fields so as to reflect local unshared settings
fields = fieldRefl.getFields();
initialized = true;
}
@ -966,6 +996,15 @@ public class ObjectStreamClass implements Serializable {
return isEnum;
}
/**
* Returns true if class descriptor represents a record type, false
* otherwise.
*/
boolean isRecord() {
requireInitialized();
return isRecord;
}
/**
* Returns true if represented class implements Externalizable, false
* otherwise.
@ -1518,6 +1557,37 @@ public class ObjectStreamClass implements Serializable {
return reflFactory.newConstructorForSerialization(cl);
}
/**
* Returns the canonical constructor for the given record class, or null if
* the not found ( which should never happen for correctly generated record
* classes ).
*/
@SuppressWarnings("preview")
private static MethodHandle canonicalRecordCtr(Class<?> cls) {
assert isRecord(cls) : "Expected record, got: " + cls;
PrivilegedAction<MethodHandle> pa = () -> {
Class<?>[] paramTypes = Arrays.stream(cls.getRecordComponents())
.map(RecordComponent::getType)
.toArray(Class<?>[]::new);
try {
Constructor<?> ctr = cls.getConstructor(paramTypes);
ctr.setAccessible(true);
return MethodHandles.lookup().unreflectConstructor(ctr);
} catch (IllegalAccessException | NoSuchMethodException e) {
return null;
}
};
return AccessController.doPrivileged(pa);
}
/**
* Returns the canonical constructor, if the local class equivalent of this
* stream class descriptor is a record class, otherwise null.
*/
MethodHandle getRecordConstructor() {
return canonicalCtr;
}
/**
* Returns non-static, non-abstract method with given signature provided it
* is defined by or accessible (via inheritance) by the given class, or
@ -1641,12 +1711,16 @@ public class ObjectStreamClass implements Serializable {
private static ObjectStreamField[] getSerialFields(Class<?> cl)
throws InvalidClassException
{
if (!Serializable.class.isAssignableFrom(cl))
return NO_FIELDS;
ObjectStreamField[] fields;
if (Serializable.class.isAssignableFrom(cl) &&
!Externalizable.class.isAssignableFrom(cl) &&
if (isRecord(cl)) {
fields = getDefaultSerialFields(cl);
Arrays.sort(fields);
} else if (!Externalizable.class.isAssignableFrom(cl) &&
!Proxy.isProxyClass(cl) &&
!cl.isInterface())
{
!cl.isInterface()) {
if ((fields = getDeclaredSerialFields(cl)) == null) {
fields = getDefaultSerialFields(cl);
}
@ -2438,4 +2512,115 @@ public class ObjectStreamClass implements Serializable {
}
}
}
/** Record specific support for retrieving and binding stream field values. */
static final class RecordSupport {
/** Binds the given stream field values to the given method handle. */
@SuppressWarnings("preview")
static MethodHandle bindCtrValues(MethodHandle ctrMH,
ObjectStreamClass desc,
ObjectInputStream.FieldValues fieldValues) {
RecordComponent[] recordComponents;
try {
Class<?> cls = desc.forClass();
PrivilegedExceptionAction<RecordComponent[]> pa = cls::getRecordComponents;
recordComponents = AccessController.doPrivileged(pa);
} catch (PrivilegedActionException e) {
throw new InternalError(e.getCause());
}
Object[] args = new Object[recordComponents.length];
for (int i = 0; i < recordComponents.length; i++) {
String name = recordComponents[i].getName();
Class<?> type= recordComponents[i].getType();
Object o = streamFieldValue(name, type, desc, fieldValues);
args[i] = o;
}
return MethodHandles.insertArguments(ctrMH, 0, args);
}
/** 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++) {
if (fields[i].isPrimitive())
primValueCount++;
else
break; // can be no more
}
return primValueCount;
}
/** Returns the default value for the given type. */
private static Object defaultValueFor(Class<?> pType) {
if (pType == Integer.TYPE)
return 0;
else if (pType == Byte.TYPE)
return (byte)0;
else if (pType == Long.TYPE)
return 0L;
else if (pType == Float.TYPE)
return 0.0f;
else if (pType == Double.TYPE)
return 0.0d;
else if (pType == Short.TYPE)
return (short)0;
else if (pType == Character.TYPE)
return '\u0000';
else if (pType == Boolean.TYPE)
return false;
else
return null;
}
/**
* Returns the stream field value for the given name. The default value
* for the given type is returned if the field value is absent.
*/
private static Object streamFieldValue(String pName,
Class<?> pType,
ObjectStreamClass desc,
ObjectInputStream.FieldValues fieldValues) {
ObjectStreamField[] fields = desc.getFields();
for (int i = 0; i < fields.length; i++) {
ObjectStreamField f = fields[i];
String fName = f.getName();
if (!fName.equals(pName))
continue;
Class<?> fType = f.getField().getType();
if (!pType.isAssignableFrom(fType))
throw new InternalError(fName + " unassignable, pType:" + pType + ", fType:" + fType);
if (f.isPrimitive()) {
if (pType == Integer.TYPE)
return Bits.getInt(fieldValues.primValues, f.getOffset());
else if (fType == Byte.TYPE)
return fieldValues.primValues[f.getOffset()];
else if (fType == Long.TYPE)
return Bits.getLong(fieldValues.primValues, f.getOffset());
else if (fType == Float.TYPE)
return Bits.getFloat(fieldValues.primValues, f.getOffset());
else if (fType == Double.TYPE)
return Bits.getDouble(fieldValues.primValues, f.getOffset());
else if (fType == Short.TYPE)
return Bits.getShort(fieldValues.primValues, f.getOffset());
else if (fType == Character.TYPE)
return Bits.getChar(fieldValues.primValues, f.getOffset());
else if (fType == Boolean.TYPE)
return Bits.getBoolean(fieldValues.primValues, f.getOffset());
else
throw new InternalError("Unexpected type: " + fType);
} else { // reference
return fieldValues.objValues[i - numberPrimValues(desc)];
}
}
return defaultValueFor(pType);
}
}
}