Skip to content

JDK 14 - JEP 359 Records

With the JDK 14 preview feature record, we can write entity class like,

Java
public record BankTransaction(LocalDate date,
                              double amount,
                              String description) {}
the compiler will generate the contsuctor, the getter methods, the 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 the toString, hashCode and equals 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
    Java
    @jdk.internal.PreviewFeature(feature=jdk.internal.PreviewFeature.Feature.RECORDS, essentialAPI=true)
    
  • It defines the equals, hashCode and toString 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.