JDK 14 - JEP 359 Records
With the JDK 14 preview feature record
, we can write entity class like,
toString
, the hashCode
and the equals
methods. Let's decompie the class to see how the methods are generated.
Java
public final class BankTransaction extends java.lang.Record {
private final java.time.LocalDate date;
private final double amount;
private final java.lang.String description;
public BankTransaction(java.time.LocalDate date, double amount, java.lang.String description) { /* compiled code */ }
public java.lang.String toString() {
return java.lang.runtime.ObjectMethods.bootstrap("toString",
new java.lang.invoke.MethodHandle[]{BankTransaction.class,
"date;amount;description", "date", "amount", "description"},
this);
}
public final int hashCode() {
return (int)java.lang.runtime.ObjectMethods.bootstrap("hashCode",
new java.lang.invoke.MethodHandle[]{BankTransaction.class,
"date;amount;description", "date", "amount", "description"},
this);
}
public final boolean equals(java.lang.Object o) {
return (boolean)java.lang.runtime.ObjectMethods.bootstrap("equals",
new java.lang.invoke.MethodHandle[]{BankTransaction.class,
"date;amount;description", "date", "amount", "description"},
this, o);
}
public java.time.LocalDate date() { /* compiled code */ }
public double amount() { /* compiled code */ }
public java.lang.String description() { /* compiled code */ }
}
You can see,
- the generated class extends
java.lang.Record
- it uses
java.lang.runtime.ObjectMethods.bootstrap
to handle thetoString
,hashCode
andequals
method
The abstract class record
Java
@jdk.internal.PreviewFeature(feature=jdk.internal.PreviewFeature.Feature.RECORDS,
essentialAPI=true)
public abstract class Record {
/**
* Constructor for record classes to call.
*/
protected Record() {}
/**
* Indicates whether some other object is "equal to" this one. In addition
* to the general contract of {@link Object#equals(Object) Object.equals},
* record classes must further obey the invariant that when
* a record instance is "copied" by passing the result of the record component
* accessor methods to the canonical constructor, as follows:
* <pre>
* R copy = new R(r.c1(), r.c2(), ..., r.cn());
* </pre>
* then it must be the case that {@code r.equals(copy)}.
*
* @implSpec
* The implicitly provided implementation returns {@code true} if
* and only if the argument is an instance of the same record type
* as this object, and each component of this record is equal to
* the corresponding component of the argument; otherwise, {@code
* false} is returned. Equality of a component {@code c} is
* determined as follows:
* <ul>
*
* <li> If the component is of a reference type, the component is
* considered equal if and only if {@link
* java.util.Objects#equals(Object,Object)
* Objects.equals(this.c(), r.c()} would return {@code true}.
*
* <li> If the component is of a primitive type, using the
* corresponding primitive wrapper class {@code PW} (the
* corresponding wrapper class for {@code int} is {@code
* java.lang.Integer}, and so on), the component is considered
* equal if and only if {@code
* PW.valueOf(this.c()).equals(PW.valueOf(r.c()))} would return
* {@code true}.
*
* </ul>
*
* The implicitly provided implementation conforms to the
* semantics described above; the implementation may or may not
* accomplish this by using calls to the particular methods
* listed.
*
* @see java.util.Objects#equals(Object,Object)
*
* @param obj the reference object with which to compare.
* @return {@code true} if this object is equal to the
* argument; {@code false} otherwise.
*/
@Override
public abstract boolean equals(Object obj);
/**
* Obeys the general contract of {@link Object#hashCode Object.hashCode}.
*
* @implSpec
* The implicitly provided implementation returns a hash code value derived
* by combining the hash code value for all the components, according to
* {@link Object#hashCode()} for components whose types are reference types,
* or the primitive wrapper hash code for components whose types are primitive
* types.
*
* @see Object#hashCode()
*
* @return a hash code value for this object.
*/
@Override
public abstract int hashCode();
/**
* Obeys the general contract of {@link Object#toString Object.toString}.
*
* @implSpec
* The implicitly provided implementation returns a string that is derived
* from the name of the record class and the names and string representations
* of all the components, according to {@link Object#toString()} for components
* whose types are reference types, and the primitive wrapper {@code toString}
* method for components whose types are primitive types.
*
* @see Object#toString()
*
* @return a string representation of the object.
*/
@Override
public abstract String toString();
}
- It has an anotation
- It defines the
equals
,hashCode
andtoString
as abstract methods.
The class java.lang.runtime.ObjectMethods
Java
@jdk.internal.PreviewFeature(feature=jdk.internal.PreviewFeature.Feature.RECORDS,
essentialAPI=false)
public class ObjectMethods {
private ObjectMethods() { }
// omit some static varaibles and methods
...
private static MethodHandle makeEquals(Class<?> receiverClass,
List<MethodHandle> getters) {
MethodType rr = MethodType.methodType(boolean.class, receiverClass, receiverClass);
MethodType ro = MethodType.methodType(boolean.class, receiverClass, Object.class);
MethodHandle instanceFalse = MethodHandles.dropArguments(FALSE, 0, receiverClass, Object.class); // (RO)Z
MethodHandle instanceTrue = MethodHandles.dropArguments(TRUE, 0, receiverClass, Object.class); // (RO)Z
MethodHandle isSameObject = OBJECT_EQ.asType(ro); // (RO)Z
MethodHandle isInstance = MethodHandles.dropArguments(CLASS_IS_INSTANCE.bindTo(receiverClass), 0, receiverClass); // (RO)Z
MethodHandle accumulator = MethodHandles.dropArguments(TRUE, 0, receiverClass, receiverClass); // (RR)Z
for (MethodHandle getter : getters) {
MethodHandle equalator = equalator(getter.type().returnType()); // (TT)Z
MethodHandle thisFieldEqual = MethodHandles.filterArguments(equalator, 0, getter, getter); // (RR)Z
accumulator = MethodHandles.guardWithTest(thisFieldEqual, accumulator, instanceFalse.asType(rr));
}
return MethodHandles.guardWithTest(isSameObject,
instanceTrue,
MethodHandles.guardWithTest(isInstance, accumulator.asType(ro), instanceFalse));
}
...
private static MethodHandle makeHashCode(Class<?> receiverClass,
List<MethodHandle> getters) {
MethodHandle accumulator = MethodHandles.dropArguments(ZERO, 0, receiverClass); // (R)I
// @@@ Use loop combinator instead?
for (MethodHandle getter : getters) {
MethodHandle hasher = hasher(getter.type().returnType()); // (T)I
MethodHandle hashThisField = MethodHandles.filterArguments(hasher, 0, getter); // (R)I
MethodHandle combineHashes = MethodHandles.filterArguments(HASH_COMBINER, 0, accumulator, hashThisField); // (RR)I
accumulator = MethodHandles.permuteArguments(combineHashes, accumulator.type(), 0, 0); // adapt (R)I to (RR)I
}
return accumulator;
}
private static MethodHandle makeToString(Class<?> receiverClass,
List<MethodHandle> getters,
List<String> names) {
// This is a pretty lousy algorithm; we spread the receiver over N places,
// apply the N getters, apply N toString operations, and concat the result with String.format
// Better to use String.format directly, or delegate to StringConcatFactory
// Also probably want some quoting around String components
assert getters.size() == names.size();
int[] invArgs = new int[getters.size()];
Arrays.fill(invArgs, 0);
MethodHandle[] filters = new MethodHandle[getters.size()];
StringBuilder sb = new StringBuilder();
sb.append(receiverClass.getSimpleName()).append("[");
for (int i=0; i<getters.size(); i++) {
MethodHandle getter = getters.get(i); // (R)T
MethodHandle stringify = stringifier(getter.type().returnType()); // (T)String
MethodHandle stringifyThisField = MethodHandles.filterArguments(stringify, 0, getter); // (R)String
filters[i] = stringifyThisField;
sb.append(names.get(i)).append("=%s");
if (i != getters.size() - 1)
sb.append(", ");
}
sb.append(']');
String formatString = sb.toString();
MethodHandle formatter = MethodHandles.insertArguments(STRING_FORMAT, 0, formatString)
.asCollector(String[].class, getters.size()); // (R*)String
if (getters.size() == 0) {
// Add back extra R
formatter = MethodHandles.dropArguments(formatter, 0, receiverClass);
}
else {
MethodHandle filtered = MethodHandles.filterArguments(formatter, 0, filters);
formatter = MethodHandles.permuteArguments(filtered, MethodType.methodType(String.class, receiverClass), invArgs);
}
return formatter;
}
public static Object bootstrap(MethodHandles.Lookup lookup, String methodName, TypeDescriptor type,
Class<?> recordClass,
String names,
MethodHandle... getters) throws Throwable {
MethodType methodType;
if (type instanceof MethodType)
methodType = (MethodType) type;
else {
methodType = null;
if (!MethodHandle.class.equals(type))
throw new IllegalArgumentException(type.toString());
}
List<MethodHandle> getterList = List.of(getters);
MethodHandle handle;
switch (methodName) {
case "equals":
if (methodType != null && !methodType.equals(MethodType.methodType(boolean.class, recordClass, Object.class)))
throw new IllegalArgumentException("Bad method type: " + methodType);
handle = makeEquals(recordClass, getterList);
return methodType != null ? new ConstantCallSite(handle) : handle;
case "hashCode":
if (methodType != null && !methodType.equals(MethodType.methodType(int.class, recordClass)))
throw new IllegalArgumentException("Bad method type: " + methodType);
handle = makeHashCode(recordClass, getterList);
return methodType != null ? new ConstantCallSite(handle) : handle;
case "toString":
if (methodType != null && !methodType.equals(MethodType.methodType(String.class, recordClass)))
throw new IllegalArgumentException("Bad method type: " + methodType);
List<String> nameList = "".equals(names) ? List.of() : List.of(names.split(";"));
if (nameList.size() != getterList.size())
throw new IllegalArgumentException("Name list and accessor list do not match");
handle = makeToString(recordClass, getterList, nameList);
return methodType != null ? new ConstantCallSite(handle) : handle;
default:
throw new IllegalArgumentException(methodName);
}
}
The method bootstrap
invokes the method makeEquals
to handle equals
, invokes the method makeHashCode
to handle hashCode
, invokes makeToString
to handle toString
.