JEP draft: Records and Sealed Types

OwnerBrian Goetz
TypeFeature
ScopeSE
StatusDraft
Componentspecification / language
EffortM
DurationM
Created2019/04/19 19:30
Updated2019/05/16 01:59
Issue8222777

Summary

Enhance the Java programming language with records and sealed types. Records provide a compact syntax for declaring classes which are transparent holders for shallowly immutable data; sealed types provide a means for declaring classes and interfaces that can restrict who their subtypes are. (Together, these two features are sometimes referred to as algebraic data types.)

Motivation and Goals

It is a common complaint that "Java is too verbose" or has too much "ceremony"; one of the worst offenders is classes that are nothing more than "plain data carriers". To write a simple data carrier class properly, one has to write a lot of low-value, repetitive, error-prone code: constructors, accessors, equals(), hashCode(), toString(), etc. (Accordingly, 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 you write most of this code, but don't do anything to help the reader distill the design intent of "I'm a plain data carrier for x, y, and z" from the dozens of lines of boilerplate. Writing Java code that models simple data aggregates should be easier -- easier to write, easier to read, and easier to be correct.

While it is superficially tempting to 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, and to organize them into related hierarchies.

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

We propose to add a new kind of type declaration to the Java Language: records. 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 key degree of freedom that classes usually enjoy: the ability to decouple a classes API from representation; in return, they gain a significant degree of concision. A record is "the state, the whole state, and nothing but the state."

A record has a name, a state description, and a body:

record Point(int x, int y) { }

Because records make the semantic claim of being simple, transparent holders for their data, we can derive most of the standard members mechanically:

The representation, and the protocols for construction, deconstruction (accessors initially, and deconstruction patterns when we have pattern matching), equality, and display are all derived from the same state description.

Records are classes; they can have most of the things other classes can: accessibility modifiers, Javadoc, annotations, an implements clause, and type variables, and the component declarations can have annotations. (The record, and the components, are implicitly final.) The body may declare static fields, static methods, static initializers, constructors, instance methods, instance initializers, and nested types.

Any of the members that are implicitly derived from the state description can also be declared explicitly. (However, carelessly implementing accessors or equals/hashCode risks undermining the semantic invariants of records.)

Further, some special consideration is provided for explicitly declaring the default constructor (the one whose signature matches that of the record's state description.) The argument list may be omitted (because it is identical to the state description); further, any record fields which are definitely unassigned on all normal completion paths are implicitly initialized from their corresponding arguments (this.x = x) on exit. This allows an explicit constructor to specify only argument validation and normalization, and omit the obvious field initialization. For example:

record Range(int lo, int hi) {
    public Range {
        if (lo > hi)
            throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi));
    }
}

A record may not extend any other class, and may not declare instance fields other than the state components declared in the record header. A record that is declared in a nested context is implicitly static.

Sealed types

A sealed type is one for which subclassing is restricted according to guidance specified with the type’s declaration. (Finality can be considered a degenerate form of sealing.)

Sealing serves two distinct purposes. The first is that it restricts which classes may be a subtype; the other is that it potentially enables exhaustiveness analysis at the use-site, such as when switching over type patterns in a sealed type.

We specify that a class is sealed by applying the sealed modifier to a class, abstract class, or interface, with an optional permits list:

sealed interface Node
     permits A, B, C { ... }

In this explicit form, Node may be extended only by the types enumerated in the permits list (which must further be members of the same package or module.) In many situations, this may be overly explicit; if all the subtypes are declared in the same compilation unit, we can omit the permits clause, in which case the compiler infers it by enumerating the subtypes in the current compilation unit.

Anonymous subclasses (and lambdas) of a sealed type are prohibited. Unless otherwise specified, abstract subtypes of sealed types are implicitly sealed, and concrete subtypes are implicitly final. This can be reversed by explicitly modifying the subtype with non-sealed.

Sealing, like finality, is both a language and JVM feature; the sealed-ness of a type, and its list of permitted subtypes, are reified in the classfile so that it can be enforced at runtime.

If we wanted to model simple arithmetic expressions with records and sealed types, it would look something like this:

sealed interface Expr { }

record ConstantExpr(int i) implements Expr { }
record PlusExpr(Expr a, Expr b) implements Expr { }
record TimesExpr(Expr a, Expr b) implements Expr { }
record NegExpr(Expr e) implements Expr { }

This declares four concrete types, ConstantExpr (which holds a single integer), PlusExpr and TimesExpr (which hold two subexpressions), and NegExpr (which holds one subexpression.) It also declares a common supertype for expressions, and captures the constraint that these are the only allowable subtypes of Expr.

Alternatives

Records can be considered a nominal form of tuples; we could implement structural tuples instead of records. However, while tuples might offer a lighter-weight means to express some aggregates, the result is often inferior aggregates. Classes and class members have meaningful names, and tuples and tuple components do not -- and a central aspect of Java's philosophy is that names matter; a Person with properties firstName and lastName is clearer and safer than a tuple of String and String. Similarly, 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. Further, classes can have behavior that is derived from their state; co-locating state and derived behavior makes it more discoverable and easier to access.

Dependencies

Records and sealed types 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 over type or deconstruction patterns.