Records

Records are a major preview feature of JDK 14. A record is a class whose state is visible to all—think of a Point with x and y coordinates. There is no need to hide them. Records make it very easy to declare such classes. A constructor, accessors, equals, hashCode, and toString come for free, and you can add other methods. Read on to find out how to work with this new feature of the Java language.

Why Records?

record

A core concept of object-oriented design is encapsulation—the hiding of private implementation details. Encapsulation enables evolution—changing the internal representation for greater efficiency or to support new features.

But sometimes, there is nothing to encapsulate. Consider your typical Point class that represents a point on a plane, with an x and a y coordinate. (Let's not get into polar coordinates here.)

Of course, you could make public instance variables

class Point {
   public double x;
   public double y;
   ...
}

In fact, java.awt.Point does just that. But then Point instances are mutable. If you want immutability, you need to provide a constructor and accessors for the coordinates. And of course you want an equals method, and then you also need a hashCode method. And maybe toString and serialization.

That's what records give you. You declare

record Point(double x, double y) {
}

and you are done.

It is envisioned that in the future, records can be used for pattern matching, with a syntax somewhat like:

switch (obj) { 
   case Point(x, 0): ... // Planned for the future—not in JDK 14
   ...
}

Of course, records have limited applicability. How limited? A report from Alan Malloy compares records with an annotation processor for a similar purpose that is used in-house at Google. From his experience, records might be about as commonly used as enum. That is a good way of thinking about records. Like enum, a record is a restricted form of a class, optimized for a specific use case. In the most common case, the declaration is as simple as it can be, and there are tweaks for customization.

What You Get for Free

When you declare a record, you get all these goodies:

Some Observations About the Basics

1. Some languages have tuples or product types. You can model a point as a pair of double. But in Java, we like names. The components should have names x and y, and we want the whole thing to be a Point, distinct from any other pairs of double.

2. A record variable holds a reference to an object. That is, records are not value or inline types—another new kid on the block. Project Valhalla lets you define

inline class Point {
   private double x;
   private double y;
   ...
}

Then a Point variable holds a flat 16 bytes of data, not a reference to an object. But the fields are still encapsulated. In time, you should be able to declare an inline record, with flat layout and no encapsulation.

3. Records are only as immutable as their fields are. Nothing stops you from forming

record Employee(String name, double salary, java.util.Date hireDate) {
}
...
var harry = new Employee("Harry Hacker", 100000, new Date(120, 0, 1));

Because java.util.Date are mutable, you can change the hireDate field:

harry.hireDate().setTime(...);

4. The implementations of hashCode, equals, and toString in the JDK are not normative. In particular, the current behavior of combining two hash codes as 31 * h1 + h2 could change. The behavior of equals is constrained by the general Object.equals contract, but there is no guarantee that the order of comparisons is fixed. You should not rely on the exact format of thetoString result either.

5. Records are a preview feature in JDK 14. To compile and run, use command-line flags like this:

javac --enable-preview --release 14 -Xlint:preview Color.java
java --enable-preview Color.java
jshell --enable-preview

Other Things That You Can Do

A record can have any number of instance methods:

record Color(int red, int green, int blue) {
    public int gray() {
        return (int)(0.2126 * red + 0.7152 * green + 0.0722 * blue);
    }
}

You can add constructors other than the canonical constructor. The first statement of such a constructor must invoke another constructor, so that ultimately the canonical constructor is invoked.

record Point(double x, double y) {
    public Point() { this(0, 0); }
}

You can provide your own implementation for any of the required instance methods:

record Point(double x, double y) {
    public String toString() { return "(" + x + ", " + y + ")"; }
}

You can also add code to the body of the generated constructor. When you do so, you don't repeat the parameter names and types:

record Color(int red, int green, int blue) {
   public Color {
      if (red < 0 || red > 255) throw new IllegalArgumentException("red out of range: " + red);
      if (green < 0 || green > 255) throw new IllegalArgumentException("green out of range: " + green);
      if (blue < 0 || blue > 255) throw new IllegalArgumentException("blue out of range: " + blue);
   }
}

You can even assign to instance variables in your constructor body, but that's probably uncommon. Any unassigned instance variables will be initialized with their parameter values.

The canonical constructor can throw checked exceptions:

record Color(int red, int green, int blue) {
   public Color throws IOException  { ... }
}

Static fields and methods are fine:

record Color(int red, int green, int blue) {
    public static Color BLACK = new Color(0, 0, 0);
    public static Color gray(int level) {
        return new Color(level, level, level);
    }
}

You can implement any interfaces:

record Point(int x, int y) implements Comparable<Point> {
    public int compareTo(Point other) {
        int dx = Integer.compare(x, other.x);
        return dx != 0 ? dx : Integer.compare(y, other.y);
    }
}

Parameterized records—no problem:

record Pair<T>(T first, T second) {
}

You can annotate everything in sight:

record @Entity Person(@NotNull String name) {
}

This annotates both the instance variable and the parameter of the canonical constructor.

What You Can't Do

Most importantly, records cannot have any instance variables other than the “record components”—the variables declared with the canonical constructor. The state of a record object is entirely determined by the record components.

A record cannot extend another class, not even another record. (Any record type implicitly extends java.lang.Record, just like any enumerated type implicitly extends java.lang.Enum. The Record superclass has no state and only abstract equals, hashCode, and toString methods.)

You cannot extend a record—it is implicitly final.

A record that is defined inside another class is automatically static. That is, it doesn't have a reference to its enclosing class (which would be an additional instance variable).

Perhaps surprisingly, reflection does not report the record components as fields.

jshell> record Point(double x, double y) {
   ...> }
jshell> Point.class.isRecord()
$1 ==> true
Point.class.getFields()
$2 ==> Field[0] {  }
Point.class.getRecordComponents()
$3 ==> RecordComponent[2] { double x, double y }

Instead, you call getRecordComponents to get an array of java.lang.reflect.RecordComponent instances. Such an instance describes the record component, just like java.lang.reflect.Field describes a field. To read the value, you first need to get the accessor value and then invoke it. Reflectively getting the value of a given field, erm, record component, is a bit of a pain:

var p = new Point(3, 4);
var accessor = Stream.of(p.getClass().getRecordComponents())
    .filter(c -> c.getName().equals("x"))
    .findFirst()
    .get()
    .getAccessor();
var value = accessor.invoke(p);

The java.beans.Introspector class knows nothing about records. It would be reasonable if the record components were reported as read-only properties, but they are not. Brian Goetz said it would be reasonable to change this.

Controversial Features

When looking at new Java features—which are coming fast and furious with the new release cadence—I am always interested in the deliberations and the design process.

There have been some controversial additions to the language, such as an expanded switch syntax that hasn't been received with unconditional love.

How about records? Just to show that one can always kvetch about something, let me double down on “record components”. Recall those are the final instance variables that are created from the canonical constructor parameters. Like, you know, the fields. The non-static fields. Why do we now have three terms—component, instance variable, field??? The field/instance variable/static variable thing has been a mess in the JLS. They should have stuck to field/instance field/static field. And now, if the record fields are really something other than final instance fields, why not record field? Adding “component” to the mix is not helpful. As I said, there is always something not to love.

More constructively, John Rose raises the issue of access modifiers. If records are meant to be dead simple, why do we have to specify public access modifiers? Can't they be inferred, like with interfaces? My guess is that this might still happen.

Wouldn't it be nice if one could use records for database entities? Of course, those are mutable. But why should the goodness of automatic method generation be reserved for immutable classes only? To stop that line of thought, JEP 359 says: It is not a goal to declare "war on boilerplate"; in particular, it is not a goal to address the problems of mutable classes using the JavaBean [sic] naming conventions.

Some people are ok with immutable records but say “whoa, it's 2020—why a constructor and not a factory method?” Like, Point.of(3, 4).

Some people are ok with immutable records but would like setter-like methods like p.x(5) that yield a new instance with the x field, erm, component, set to 5 and all others unchanged. This enhancement could potentially come in the future.

And how immutable are record instances really, when you can mutate the instance fields, erm, components? Shouldn't those always be immutable as well? Unfortunately, Java has no way of expressing that a class is immutable, so that's not on the horizon.

What about nulls? Shouldn't there be a simpler way of preventing them than adding an explicit null check to the canonical constructor?

All this is pretty tame and in line what you'd expect with any new Java feature. Concerns about mutability, nulls, constructors vs. factories. No pitchforks this time. But really...record component?

Comments powered by Talkyard.