JEP 359: Records (Preview)
Summary
Enhance the Java programming language with records. Records provide a compact syntax for declaring classes which are transparent holders for shallowly immutable data. This is a preview language feature in JDK 14.
Motivation and Goals
It is a common complaint that "Java is too verbose" or has too much "ceremony". Some of the worst offenders are classes that are nothing more than plain "data carriers" that serve as simple aggregates. To write a data carrier class properly, one has to write a lot of low-value, repetitive, error-prone code: constructors, accessors, equals()
, hashCode()
, toString()
, etc. Developers are sometimes tempted to cut corners such as omitting these important methods (leading to surprising behavior or poor debuggability), or pressing an alternate but not entirely appropriate class into service (because it has the "right shape" and they don't want to declare yet another class).
IDEs will help write most of the code in a data carrier class, but don't do anything to help the reader distill the design intent of "I'm a data carrier for x
, y
, and z
" from the dozens of lines of boilerplate. Writing Java code that models simple aggregates should be easier -- to write, to read, and to verify as correct.
While it is superficially tempting to treat records as primarily being about boilerplate reduction, we instead choose a more semantic goal: modeling data as data. (If the semantics are right, the boilerplate will take care of itself.) It should be easy, clear, and concise to declare shallowly-immutable, well-behaved nominal data aggregates.
Non-Goals
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 naming conventions. It is not a goal to add features such as properties, metaprogramming, and annotation-driven code generation, even though they are frequently proposed as "solutions" to this problem.
Description
Records are a new kind of type declaration in the Java language. Like an enum
, a record
is a restricted form of class. It declares its representation, and commits to an API that matches that representation. Records give up a freedom that classes usually enjoy: the ability to decouple API from representation. In return, records gain a significant degree of concision.
A record has a name and a state description. The state description declares the components of the record. Optionally, a record has a body. For example:
record Point(int x, int y) { }
Because records make the semantic claim of being simple, transparent holders for their data, a record acquires many standard members automatically:
- A private final field for each component of the state description;
- A public read accessor method for each component of the state description, with the same name and type as the component;
- A public constructor, whose signature is the same as the state description, which initializes each field from the corresponding argument;
- Implementations of
equals
andhashCode
that say two records are equal if they are of the same type and contain the same state; and - An implementation of
toString
that includes the string representation of all the record components, with their names.
In other words, the representation of a record is derived mechanically and completely from the state description, as are the protocols for construction, deconstruction (accessors initially, and deconstruction patterns when we have pattern matching), equality, and display.
Restrictions on records
Records cannot extend any other class, and cannot declare instance fields other than the private final fields which correspond to components of the state description. Any other fields which are declared must be static. These restrictions ensure that the state description alone defines the representation.
Records are implicitly final, and cannot be abstract. These restrictions emphasize that the API of a record is defined solely by its state description, and cannot be enhanced later by another class or record.
The components of a record are implicitly final. This restriction embodies an immutable by default policy that is widely applicable for data aggregates.
Beyond the restrictions above, records behave like normal classes: they can be declared top level or nested, they can be generic, they can implement interfaces, and they are instantiated via the new
keyword. The record's body may declare static methods, static fields, static initializers, constructors, instance methods, and nested types. The record, and the individual components in a state description, may be annotated. If a record is nested, then it is implicitly static; this avoids an immediately enclosing instance which would silently add state to the record.
Explicitly declaring members of a record
Any of the members that are automatically derived from the state description can also be declared explicitly. However, carelessly implementing accessors or equals
/hashCode
risks undermining the semantic invariants of records.
Special consideration is provided for explicitly declaring the canonical constructor (the one whose signature matches the record's state description). The constructor may be declared without a formal parameter list (in this case, it is assumed identical to the state description), and any record fields which are definitely unassigned when the constructor body completes normally are implicitly initialized from their corresponding formal parameters (this.x = x
) on exit. This allows an explicit canonical constructor to perform only validation and normalization of its parameters, and omit the obvious field initialization. For example:
record Range(int lo, int hi) {
public Range {
if (lo > hi) /* referring here to the implicit constructor parameters */
throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi));
}
}
Grammar
RecordDeclaration:
{ClassModifier} record TypeIdentifier [TypeParameters]
(RecordComponents) [SuperInterfaces] [RecordBody]
RecordComponents:
{RecordComponent {, RecordComponent}}
RecordComponent:
{Annotation} UnannType Identifier
RecordBody:
{ {RecordBodyDeclaration} }
RecordBodyDeclaration:
ClassBodyDeclaration
RecordConstructorDeclaration
RecordConstructorDeclaration:
{Annotation} {ConstructorModifier} [TypeParameters] SimpleTypeName
[Throws] ConstructorBody
Annotations on record components
Declaration annotations are permitted on record components if they are applicable to record components, parameters, fields, or methods. Declaration annotations that are applicable to any of these targets are propagated to implicit declarations of any mandated members.
Type annotations that modify the types of record components are propagated to the types in implicit declarations of mandated members (e.g., constructor parameters, field declarations, and method declarations). Explicit declarations of mandated members must match the type of the corresponding record component exactly, not including type annotations.
Reflection API
The following public methods will be added to java.lang.Class
:
RecordComponent[] getRecordComponents()
boolean isRecord()
The method getRecordComponents()
returns an array of java.lang.reflect.RecordComponent
objects, where java.lang.reflect.RecordComponent
is a new class. The elements of this array correspond to the record’s components, in the same order as they appear in the record declaration. Additional information can be extracted from each RecordComponent
in the array, including its name, type, generic type, annotations, and its accessor method.
The method isRecord()
returns true if the given class was declared as a record. (Compare with isEnum()
.)
Alternatives
Records can be considered a nominal form of tuples. Instead of records, we could implement structural tuples. However, while tuples might offer a lighterweight means of expressing some aggregates, the result is often inferior aggregates:
-
A central aspect of Java's philosophy is that names matter. Classes and their members have meaningful names, while tuples and tuple components do not. That is, a
Person
class with propertiesfirstName
andlastName
is clearer and safer than an anonymous tuple ofString
andString
. -
Classes support state validation through their constructors; tuples do not. Some data aggregates (such as numeric ranges) have invariants that, if enforced by the constructor, can thereafter be relied upon; tuples do not offer this ability.
-
Classes can have behavior that is based on their state; co-locating the state and behavior makes the behavior more discoverable and easier to access. Tuples, being raw data, offer no such facility.
Dependencies
Records go well with sealed types (JEP 360); records and sealed types taken together form a construct often referred to as algebraic data types. Further, records lend themselves naturally to pattern matching. Because records couple their API to their state description, we will eventually be able to derive deconstruction patterns for records as well, and use sealed type information to determine exhaustiveness in switch
expressions with type patterns or deconstruction patterns.