Records are a major preview feature of JDK 14. A record is a class whose state is visible to all—think of a
Point
withx
andy
coordinates. There is no need to hide them. Records make it very easy to declare such classes. A constructor, accessors,equals
,hashCode
, andtoString
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.
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.
When you declare a record, you get all these goodies:
Point
instance as new Point(3, 4)
.p
is a Point
, you get the coordinates as p.x()
and p.y()
. (Note: Not p.getX()
.)equals
, hashCode
, toString
. For example, p.toString()
yields the string "Point[x=1.0, y=0.0]"
. Serializable
interface:
record Point(double x, double y) implements Serializable { }
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
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.
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.
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).
The canonical constructor cannot throw checked exceptions:
record SleepyPoint(double x, double y) { public SleepyPoint throws InterruptedException { // Error Thread.sleep(1000); } }
A new method can tell whether a Class
instance is a record:
jshell> record Point(double x, double y) {} jshell> Point.class.isRecord() $1 ==> true
Reflection reports the record components as private fields.
jshell> Point.class.getDeclaredFields() Field[2] { private final double Point.x, private final double Point.y }
You can also 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.
jshell> Point.class.getRecordComponents() $3 ==> RecordComponent[2] { double x, double y }
To read the value of a component reflectively, you can get the accessor method from the RecordComponent
object:
var p = new Point(3, 4); RecordComponent rc = p.getClass().getRecordComponents()[0]; Object value = rc.getAccessor().invoke(p);
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?
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.
All this is pretty tame and in line what you'd expect with any new Java feature. Concerns about mutability, nulls, constructors vs. factories, beans. No pitchforks this time. But really...record component?
Comments powered by Talkyard.