JEP draft: Records for the Java Language (Preview)

AuthorBrian Goetz
OwnerVicente Arturo Romero Zaldivar
TypeFeature
ScopeSE
StatusSubmitted
Componentspecification / language
EffortM
DurationM
Reviewed byAlex Buckley
Created2019/04/19 19:30
Updated2019/08/19 20:50
Issue8222777

Summary

Enhance the Java programming language with records. Records provide a compact syntax for declaring classes which are transparent holders for shallowly immutable data.

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 to 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:

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 assure 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 new. The record's body may declare static methods, static fields, static initializers, constructors, instance methods, instance initializers, 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 any of { record components, parameters, fields, 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, 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 have been added to java.lang.Class:

The method getRecordComponents() returns an array of java.lang.reflect.RecordComponent, where java.lang.reflect.RecordComponent is a new class, corresponding to the record components, in the same order as they appear in the record declaration. Additional information can be extracted from each java.lang.reflect.RecordComponent in the array, including 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:

Dependencies

Records go well with sealed types; 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.