benf.org :  other :  cfr :  records

Records

As of Java 14, the 'record' type has entered preview state.

There's a lot written about it, the JEP - https://openjdk.java.net/jeps/359), or this writeup, so I won't go in to what a record is, but it's interesting to see what it's actually composed of.

Spoiler alert - it feels a lot like the way enum was implemented - compiler generated boilerplate ;) )

A basic record

record RecordTest1 (String firstName, String lastName) {}

Decompiled with CFR (default behaviour), we get back .... (drum roll)

/*
 * Decompiled with CFR 0.149.
 */
package org.benf.cfr.tests;

record RecordTest1(String firstName, String lastName) {
}

Ok, that's a bit boring, what if we turn OFF record resugaring? (using --recordtypes false;)

> java -jar cfr-0.149.jar RecordTest1.class --recordtypes false

/*
 * Decompiled with CFR 0.149.
 */
package org.benf.cfr.tests;

import java.lang.invoke.MethodHandle;
import java.lang.runtime.ObjectMethods;

final class RecordTest1
extends Record {
    private final String firstName;
    private final String lastName;

    public RecordTest1(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return ObjectMethods.bootstrap("toString", new MethodHandle[]{RecordTest1.class, "firstName;lastName", "firstName", "lastName"}, this);
    }

    @Override
    public final int hashCode() {
        return (int)ObjectMethods.bootstrap("hashCode", new MethodHandle[]{RecordTest1.class, "firstName;lastName", "firstName", "lastName"}, this);
    }

    @Override
    public final boolean equals(Object o) {
        return (boolean)ObjectMethods.bootstrap("equals", new MethodHandle[]{RecordTest1.class, "firstName;lastName", "firstName", "lastName"}, this, o);
    }

    public String firstName() {
        return this.firstName;
    }

    public String lastName() {
        return this.lastName;
    }
}

It's implemented as 'just' a class, that extends java.lang.Record.

What about a few non-defaults?

We can override behaviour of any of these methods, add extra code into the constructor, and even chain constructors

record RecordTest9 (String firstName, String lastName, RecordTest2 child) {
    static int x = 1;

    public RecordTest9 {
        if (firstName == lastName) {
            lastName = "McPointer";
        }
    }

    public RecordTest9(String one) {
        this(one.split(" ")[0], one.split(" ")[1], null);
    }

    public static void doX() {
        x++;
    }

    @Override
    public final int hashCode() {
        return 3;
    }

    @Override
    public String toString() {
        return lastName + "." + firstName;
    }

}

Just to check, CFR does get this back nicely .....

/*
 * Decompiled with CFR 0.149.
 */
package org.benf.cfr.tests;

import org.benf.cfr.tests.RecordTest2;

record RecordTest9(String firstName, String lastName, RecordTest2 child) {
    static int x = 1;

    public RecordTest9 {
        if (firstName == lastName) {
            lastName = "McPointer";
        }
    }

    public RecordTest9(String one) {
        this(one.split(" ")[0], one.split(" ")[1], null);
    }

    public static void doX() {
        ++x;
    }

    @Override
    public final int hashCode() {
        return 3;
    }

    @Override
    public String toString() {
        return this.lastName + "." + this.firstName;
    }
}

And again, here's what we get if we turn off record re-sugaring. Not unexpected that the member assignments (those which we don't override) are done at the bottom of the canonical constructor.

>java -jar cfr-0.149.jar RecordTest9.class --recordtypes false

/*
 * Decompiled with CFR 0.149.
 */
package org.benf.cfr.tests;

import java.lang.invoke.MethodHandle;
import java.lang.runtime.ObjectMethods;
import org.benf.cfr.tests.RecordTest2;

final class RecordTest9
extends Record {
    private final String firstName;
    private final String lastName;
    private final RecordTest2 child;
    static int x = 1;

    public RecordTest9(String firstName, String lastName, RecordTest2 child) {
        if (firstName == lastName) {
            lastName = "McPointer";
        }
        this.firstName = firstName;
        this.lastName = lastName;
        this.child = child;
    }

    public RecordTest9(String one) {
        this(one.split(" ")[0], one.split(" ")[1], null);
    }

    public static void doX() {
        ++x;
    }

    @Override
    public final int hashCode() {
        return 3;
    }

    @Override
    public String toString() {
        return this.lastName + "." + this.firstName;
    }

    @Override
    public final boolean equals(Object o) {
        return (boolean)ObjectMethods.bootstrap("equals", new MethodHandle[]{RecordTest9.class, "firstName;lastName;child", "firstName", "lastName", "child"}, this, o);
    }

    public String firstName() {
        return this.firstName;
    }

    public String lastName() {
        return this.lastName;
    }

    public RecordTest2 child() {
        return this.child;
    }
}

Observations

  • Canonical constructor arguments can be given annotations, in which case they can't be hidden in reconstruction
  • A canonical constructor with 'implied' arguments is NOT QUITE the same as one with explicit.

    This won't compile in 14-ea+34-1452, complaining about potentially uninitialised firstname. This might be a javac bug?

        public RecordTest9(String firstName, String lastName, RecordTest2 child) {
            if (firstName == lastName) {
                lastName = "McPointer";
            }
        }
    

    Last updated 02/2020