andrew Flower

Java Records

Immutable, Simple, Clean

This article is just aimed to be an introduction to the upcoming JDK feature called "Records". They are sometimes called "Record Classes". Records were first available as a preview feature in JDK 14 ( JEP359), returned for a second preview in JDK 15 ( JEP384), and is expected to have its be released in JDK 16 ( JEP395).

The goal of Records are to provide a minimal way to declare immutable classes. It allows declaration of types with a canonical set of values, while implementing all the obvious boilerplate code (accessors, hashCode, equals, toString) automatically. Specifically the main goals described by the JEP are:

  • Devise an object-oriented construct that expresses a simple aggregation of values.
  • Help developers to focus on modeling immutable data rather than extensible behavior.
  • Automatically implement data-driven methods such as equals and accessors.
  • Preserve long-standing Java principles such as nominal typing and migration compatibility.
record Coordinate(int x, int y) {}

This article was written using the following versions:

Java 15

Making Immutable Classes

With Java currently, in order to create classes for which instances are immutable, we have to implement a lot of the standard boilerplate as for mutable classes. We make fields final, initialized in the constructor and do not provide mutator methods.

public class MinMax {
    private final int min;
    private final int max;

    public MinMax(final int min, final int max) {
        this.min = min;
        this.max = max;
    }

    public int min() {
        return min;
    }

    public int max() {
        return max;
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        final MinMax other = (MinMax) o;
        return min == other.min && max == other.max;
    }

    @Override
    public int hashCode() {
        return Objects.hash(min, max);
    }

    @Override
    public String toString() {
        return "MinMaxOld{min=" + min + ", max=" + max + '}';
    }
}

Having to do this for every class is problematic for a number reasons, including:

  • It's a waste of time to write. This is standard boilerplate, that most IDEs generate anyway
  • It's a waste of time to read
  • In order to tell if this class is just using standard implementations for boilerplate methods, readers must read over the whole class. Only to find it's standard boilerplate.

Enter Records

Below is the equivalent using the new record feature. It allows us to create clean, single purpose classes with a clear set of component values.

record MinMax(int min, int max) {}

Above we've created a Record with min and max integer components. With this Record, we automatically get a standard set of implementations:

  • Canonical Constructor
  • Accessor methods (min() and max()) for each component
  • equals()
  • hashCode()
  • toString()

A record is therefore safe to use in collections.

Beware that this does not ensure that components are immutable. If a component is modified, this could have impact on the hashCode, and relative behavior of equals.

Some other notes about Records:

  • They can implement interfaces
  • You can declare static and non-static methods in records.
  • You can declare other constructors, but all constructors must call the canonical constructor. This ensures a record instance is always constructed in a canonical/standard way
  • Instance fields cannot be specified apart from those in the Record header
  • Records are implicitly classes inheriting the Record abstract class.
  • Records can be created in local scopes (inside methods) when needed temporarily.
  • Record definitions nested in other classes/records are implicitly static.

I recommend reading the JEP for a full explanation about Records.

Enabling Preview Features

Because Records are not a release feature yet, only expect in JDK 16, usage needs to be explicitly enabled. You can enable preview features when compiling by using the --enable-preview flag:

javac --source 15 --enable-preview RecordExample.java

Equivalently, when running the Java application, you need to enable preview features:

java --enable-preview RecordExample

With Gradle

If you're using Gradle to build, or run tests you can enable them as follows

// Compile with preview features
tasks.withType(JavaCompile) {
    options.compilerArgs += '--enable-preview'
}

// Run Tests with Preview features enabled
test {
    useJUnitPlatform()
    jvmArgs(['--enable-preview'])
}

Usage Examples

This isn't rocket science, but for completeness here are some examples that show benefits from the simplicity of Records.

Multiple Returns

In other languages like Python or Ruby you can easily create tuple, list or array literals. This allows functions to easily return multiple items without much hassle. In Java there are no type-agnostic lists or tuples, and creating classes just for returning something seems overkill.

Records really come to the rescue here, as we can easily create composites of data in order to return multiple items. And even better than nameless, typeless tuple values in Python or Ruby, with Records we have meaning and type safety.

record SearchResult(Person person, int index) {}

static Optional<SearchResult> findByFirstName(List<Person> people, String firstname) {
    int i = 0;
    for (Person person : people) {
        if (firstname.equals(person.firstName)) {
            return Optional.of(new SearchResult(person, i));
        }
        i++;
    }
    return Optional.empty();
}

Here we create a nested Record to allow for returning the matched Person along with the index at which it was found.

In Collections

Because of the auto-generated equals() and hashCode() methods, Records make it really easy to make temporary value types for inserting into collections like Sets or Maps.

For example, when implementing a BFS search on a grid we could create local Records in order to store our cells and also cells with distance. We can easily just use them in the Set and trust the hashCode and equals to work.

static int bfsShortestPath(final char[][] map, int startX, int startY) {
    record Cell(int x, int y) {}
    record CellDist(Cell cell, int distance) {}
    ArrayDeque<CellDist> queue = new ArrayDeque<>();
    Set<Cell> visited = new HashSet<>();
    queue.offer(new CellDist(new Cell(startX, startY), 0));

    while(!queue.isEmpty()) {
        CellDist current = queue.poll();
        if (visited.contains(current)) continue;
        visited.add(current.cell);

        if (map[current.cell.y][current.cell.x] == 'X') {
            return current.distance;
        }

        if (/*in bounds*/) {
            queue.offer(new CellDist(new Cell(current.cell.x + 1, current.cell.y),
                                     distance + 1
            ));
        }
        //...other neighbours
    }
    return -1;
}

Also note above how the Records are defined locally within the scope of that method. And because of their succinct form, provide a lot of convenience without adding too much clutter. The visited Set above is used to track which Cells have been processed already. The record CellDist provides a way to compose Cell and the distance from the start.

More To Come

Note that the below functionality is not available even in preview yet, but it should be coming in some or other form, and it's quite elegant! Java is improving fast ; it's exciting!

I'm not in any way affiliated with the JDK team so take these ponderings with a grain of salt!

Deconstructing

Records are still to come out in their first version. In the Inside Java Podcast about Records with Gavin Bierman I heard something quite exciting about the future of Records and another Preview feature "Pattern Matching", which will allow pattern-matching and record component deconstruction in one line, within a limited scope. I would imagine it to look something like this:

if (shape instanceof Rectangle(Point(var x, var y), Point(var u, var v))) {
    System.out.println("Is a Rectangle with width: " + Math.abs(u-x));
}

Exhaustive switch statements

Reading this article on Pattern Matching there's a mention of the combination of 3 new features (Records, Pattern-Matching and Sealed classes) that will allow switch statements to exhaustively switch on a type without missing subtypes:

sealed interface Shape permits Rectangle, Circle { }

record Rectangle(int left, int right, int top, int bottom) implements Shape {}
record Circle(int radius, Point center) implements Shape {}
...
Point center(Shape shape) {
    return switch(shape) {
        case Rectangle(int left, int right, int top, int bottom): return new Point((right-left)/2, (bottom-top)/2);
        case Circle(int radius, Point center): return center;
    }
}

At compile-time, it can be confirmed whether your cases exhaust all the subtypes and we can also deconstruct each subtype respective to their canonical component definitions.

Let me re-iterate that this section was just a bit of premature excitement about future improvements to these preview features. They are not available in any JDK releases currently, and will probably not be in the exact form I've imagined, if at all.

Summary

In the above we learned the following:

  • How Records will be coming to the JDK to provide an easy, standard way to create immutable classes
  • How to try out Records as preview features in JDK 14, 15
  • That very usable value classes can be created with concise syntax
  • A little about what else might be coming in future improvements to Records in conjunction with other features like Pattern Matching

References