Records (JEP 395)
Records were a major preview feature of JDK 14 and are an official feature since JDK 16. A record is an immutable 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.
Why Records?
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.
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.
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:
- A private instance variable for each of the variables in the thing-that-looks-like-a-constructor-heading after the record name
- A “canonical” constructor that sets all instance variables. You construct a
Point
instance asnew Point(3, 4)
. - Accessors for each instance variable. If
p
is aPoint
, you get the coordinates asp.x()
andp.y()
. (Note: Notp.getX()
.) - Public methods
equals
,hashCode
,toString
. For example,p.toString()
yields the string"Point[x=1.0, y=0.0]"
. - Serialization provided the record implements the
Serializable
interface:
Other Things That You Can Do
A record can have any number of instance methods:
You can provide your own implementation for any of the required instance methods:
Static fields and methods are fine:
You can implement any interfaces:
Records can be local—defined inside a method—just like local classes:
Parameterized records — no problem:
Constructors: Canonical, Custom, and Compact
Every record has a canonical constructor that sets all instance variables.
You can add “custom” constructors in addition to the canonical constructor. The first statement of such a constructor must invoke another constructor, so that ultimately the canonical constructor is invoked. The following record has two constructors: the canonical constructor and a custom constructor yielding the origin.
You can also provide your own implementation of the canonical constructor. When you do so, you can declare the constructor in the usual way:
This is rather verbose and not what you want to do in practice. Instead, you should use the “compact” form. Omit the constructor parameters and the instance variable initialization:
You can modify the constructor parameters before they are assigned to the instance variables. Here we normalize an angle in polar coordinates so that it is between 0 and 2π:
Note that the assignment of the parameters r
and theta
to the instance variables this.r
and this.theta
happens at the end of the canonical constructor. You cannot read or modify the instance variables in the body 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.
You cannot extend a record — it is implicitly final
.
A record cannot extend another class, not even another record. (Any record 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.)
There are no “inner records”. A record that is defined inside another class or method 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); } }
Reflection
The isRecord
method can tell whether a Class
instance is a record.
Reflection reports the record components as private fields.
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.
To read the value of a component reflectively, you can get the accessor method from the RecordComponent
object.
Some Further Observations
1. Some languages have tuples or product types. In those languages, you can model a point as a pair of double
. But in Java, we like names. Point 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 will let 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 having mutable components:
Because arrays are mutable, you can change the elements of coordinates
.
This is not a good idea, but the Java language won’t stop you. In general, Java has no mechanism for expressing immutability. It is no different with reccords.
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 the toString
result either.
5. It is envisioned that in the future, records can be used for pattern matching, with a syntax somewhat like:
switch (obj) { case instanceof Point(x, 0) p: ... // Maybe the future - not in JDK 16 ... }