Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8222777

JEP 359: Records (Preview)

    XMLWordPrintable

Details

    • Brian Goetz
    • Feature
    • Open
    • SE
    • amber dash dev at openjdk dot java dot net
    • M
    • M
    • 359

    Description

      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 and hashCode 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 properties firstName and lastName is clearer and safer than an anonymous tuple of String and String.

      • 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.

      Attachments

        Issue Links

          Activity

            People

              vromero Vicente Arturo Romero Zaldivar
              briangoetz Brian Goetz
              Vicente Arturo Romero Zaldivar Vicente Arturo Romero Zaldivar
              Alex Buckley
              Votes:
              0 Vote for this issue
              Watchers:
              16 Start watching this issue

              Dates

                Created:
                Updated:
                Resolved: