Java Record Classes
Java records classes; like an enum
, a record
is a restricted form of class. It declares its representation, and commits to an API that matches that representation. We pair this with another abstraction, sealed types, which can assert control over which other types may be its subclasses. (A final
class is the ultimate form of sealed class; it permits no subtypes at all.)
Enhance the Java programming language with records, which are classes that act as transparent carriers for immutable data. Records can be thought of as nominal tuples
History
Records were proposed by JEP 359 and delivered in JDK 14 as a preview feature.
In response to feedback, the design was refined by JEP 384 and delivered in JDK 15 as a preview feature for a second time. The refinements for the second preview were as follows:
In the first preview, canonical constructors were required to be
public
. In the second preview, if the canonical constructor is implicitly declared then its access modifier is the same as the record class; if the canonical constructor is explicitly declared then its access modifier must provide at least as much access as the record class.The meaning of the
@Override
annotation was extended to include the case where the annotated method is an explicitly declared accessor method for a record component.To enforce the intended use of compact constructors, it became a compile-time error to assign to any of the instance fields in the constructor body.
The ability to declare local record classes, local enum classes, and local interfaces was introduced.
This JEP proposes to finalize the feature in JDK 16, with the following refinement:
- Relax the longstanding restriction whereby an inner class cannot declare a member that is explicitly or implicitly static. This will become legal and, in particular, will allow an inner class to declare a member that is a record class.
Additional refinements may be incorporated based on further feedback.
Goals
Devise an object-oriented construct that expresses a simple aggregation of values.
Help developers to focus on modeling immutable data rather than extensible behavior.
Automatically implement data-driven methods such as
equals
and accessors.Preserve long-standing Java principles such as nominal typing and migration compatibility.
Non-Goals
- While records do offer improved concision when declaring data carrier classes, it is not a goal to declare a “war on boilerplate”. In particular, it is not a goal to address the problems of mutable classes which use the JavaBeans naming conventions.
- It is not a goal to add features such as properties or annotation-driven code generation, which are often proposed to streamline the declaration of classes for “Plain Old Java Objects”.
For example, here is a record class with two fields:
record Rectangle(double length, double width) { }
This concise declaration of a rectangle is equivalent to the following normal class:
public final class Rectangle {
private final double length;
private final double width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
double length() { return this.length; }
double width() { return this.width; }
// Implementation of equals() and hashCode(), which specify
// that two record objects are equal if they
// are of the same type and contain equal field values.
public boolean equals...
public int hashCode...
// An implementation of toString() that returns a string
// representation of all the record class's fields,
// including their names.
public String toString() {...}
}
A record class declaration consists of a name; optional type parameters (generic record declarations are supported); a header, which lists the “components” of the record; and a body.
A record class declares the following members automatically:
- For each component in the header, the following two members:
- A
private
final
field with the same name and declared type as the record component. This field is sometimes referred to as a component field. - A
public
accessor method with the same name and type of the component; in theRectangle
record class example, these methods areRectangle::length()
andRectangle::width()
.
- A
- A canonical constructor whose signature is the same as the header. This constructor assigns each argument from the
new
expression that instantiates the record class to the corresponding component field. - Implementations of the
equals
andhashCode
methods, which specify that two record classes are equal if they are of the same type and contain equal component values. - An implementation of the
toString
method that includes the string representation of all the record class’s components, with their names.
As record classes are just special kinds of classes, you create a record object (an instance of a record class) with the new
keyword, for example:
Rectangle r = new Rectangle(4,5);
The Canonical Constructor of a Record Class
The following example explicitly declares the canonical constructor for the Rectangle
record class. It verifies that length
and width
are greater than zero. If not, it throws an IllegalArgumentException:
record Rectangle(double length, double width) {
public Rectangle(double length, double width) {
if (length <= 0 || width <= 0) {
throw new java.lang.IllegalArgumentException(
String.format("Invalid dimensions: %f, %f", length, width));
}
this.length = length;
this.width = width;
}
}
Repeating the record class’s components in the signature of the canonical constructor can be tiresome and error-prone. To avoid this, you can declare a compact constructor whose signature is implicit (derived from the components automatically).
For example, the following compact constructor declaration validates length
and width
in the same way as in the previous example:
record Rectangle(double length, double width) {
public Rectangle {
if (length <= 0 || width <= 0) {
throw new java.lang.IllegalArgumentException(
String.format("Invalid dimensions: %f, %f", length, width));
}
}
}
This succinct form of constructor declaration is only available in a record class. Note that the statements this.length = length;
and this.width = width;
which appear in the canonical constructor do not appear in the compact constructor. At the end of a compact constructor, its implicit formal parameters are assigned to the record class’s private fields corresponding to its components.
Explicit Declaration of Record Class Members
You can explicitly declare any of the members derived from the header, such as the public
accessor methods that correspond to the record class’s components, for example:
record Rectangle(double length, double width) {
// Public accessor method
public double length() {
System.out.println("Length is " + length);
return length;
}
}
If you implement your own accessor methods, then ensure that they have the same characteristics as implicitly derived accessors (for example, they’re declared public
and have the same return type as the corresponding record class component). Similarly, if you implement your own versions of the equals
, hashCode
, and toString
methods, then ensure that they have the same characteristics and behavior as those in the java.lang.Record
class, which is the common superclass of all record classes.
You can declare static fields, static initializers, and static methods in a record class, and they behave as they would in a normal class, for example:
record Rectangle(double length, double width) {
// Static field
static double goldenRatio;
// Static initializer
static {
goldenRatio = (1 + Math.sqrt(5)) / 2;
}
// Static method
public static Rectangle createGoldenRectangle(double width) {
return new Rectangle(width, width * goldenRatio);
}
}
You cannot declare instance variables (non-static fields) or instance initializers in a record class.
For example, the following record class declaration doesn’t compile:
record Rectangle(double length, double width) {
// Field declarations must be static:
BiFunction<Double, Double, Double> diagonal;
// Instance initializers are not allowed in records:
{
diagonal = (x, y) -> Math.sqrt(x*x + y*y);
}
}
You can declare instance methods in a record class, independent of whether you implement your own accessor methods. You can also declare nested classes and interfaces in a record class, including nested record classes (which are implicitly static). For example:
record Rectangle(double length, double width) {
// Nested record class
record RotationAngle(double angle) {
public RotationAngle {
angle = Math.toRadians(angle);
}
}
// Public instance method
public Rectangle getRotatedRectangleBoundingBox(double angle) {
RotationAngle ra = new RotationAngle(angle);
double x = Math.abs(length * Math.cos(ra.angle())) +
Math.abs(width * Math.sin(ra.angle()));
double y = Math.abs(length * Math.sin(ra.angle())) +
Math.abs(width * Math.cos(ra.angle()));
return new Rectangle(x, y);
}
}
You cannot declare native
methods in a record class.
Features of Record Classes
A record class is implicitly final
, so you cannot explicitly extend a record class. However, beyond these restrictions, record classes behave like normal classes:
You can create a generic record class, for example:
record Triangle<C extends Coordinate> (C top, C left, C right) { }
You can declare a record class that implements one or more interfaces, for example:
record Customer(...) implements Billable { }
You can annotate a record class and its individual components, for example:
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface GreaterThanZero { }record Rectangle(
@GreaterThanZero double length,
@GreaterThanZero double width) { }If you annotate a record component, then the annotation may be propagated to members and constructors of the record class. This propagation is determined by the contexts in which the annotation interface is applicable. In the previous example, the
@Target(ElementType.FIELD)
meta-annotation means that the@GreaterThanZero
annotation is propagated to the field corresponding to the record component. Consequently, this record class declaration would be equivalent to the following normal class declaration:public final class Rectangle {
private final @GreaterThanZero double length;
private final @GreaterThanZero double width;public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}double length() { return this.length; }
double width() { return this.width; }
}
Record Classes and Sealed Classes and Interfaces
Record classes work well with sealed classes and interfaces. See below for an example:
You can name a record class in the permits
clause of a sealed class or interface. See Record Classes for more information.
Record classes are implicitly final
, so you can implement the previous example with record classes instead of ordinary classes:
package com.techshitanshu.example.records.expressions;
public class TestExpressions {
public static void main(String[] args) {
// (6 + 7) * -8
System.out.println(
new TimesExpr(
new PlusExpr(new ConstantExpr(6), new ConstantExpr(7)),
new NegExpr(new ConstantExpr(8))
).eval());
}
}
sealed interface Expr
permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {
public int eval();
}
record ConstantExpr(int i) implements Expr {
public int eval() { return i(); }
}
record PlusExpr(Expr a, Expr b) implements Expr {
public int eval() { return a.eval() + b.eval(); }
}
record TimesExpr(Expr a, Expr b) implements Expr {
public int eval() { return a.eval() * b.eval(); }
}
record NegExpr(Expr e) implements Expr {
public int eval() { return -e.eval(); }
}
Local Record Classes
A local record class is similar to a local class; it’s a record class defined in the body of a method.
In the following example, a merchant is modeled with a record class, Merchant
. A sale made by a merchant is also modeled with a record class, Sale
. Both Merchant
and Sale
are top-level record classes. The aggregation of a merchant and their total monthly sales is modeled with a local record class, MonthlySales
, which is declared inside the findTopMerchants
method. This local record class improves the readability of the stream operations that follow:
import java.time.*;
import java.util.*;
import java.util.stream.*;
record Merchant(String name) { }
record Sale(Merchant merchant, LocalDate date, double value) { }
public class MerchantExample {
List<Merchant> findTopMerchants(
List<Sale> sales, List<Merchant> merchants, int year, Month month) {
// Local record class
record MerchantSales(Merchant merchant, double sales) {}
return merchants.stream()
.map(merchant -> new MerchantSales(
merchant, this.computeSales(sales, merchant, year, month)))
.sorted((m1, m2) -> Double.compare(m2.sales(), m1.sales()))
.map(MerchantSales::merchant)
.collect(Collectors.toList());
}
double computeSales(List<Sale> sales, Merchant mt, int yr, Month mo) {
return sales.stream()
.filter(s -> s.merchant().name().equals(mt.name()) &&
s.date().getYear() == yr &&
s.date().getMonth() == mo)
.mapToDouble(s -> s.value())
.sum();
}
public static void main(String[] args) {
Merchant sneha = new Merchant(“Sneha”);
Merchant raj = new Merchant(“Raj”);
Merchant florence = new Merchant(“Florence”);
Merchant leo = new Merchant(“Leo”);
List<Merchant> merchantList = List.of(sneha, raj, florence, leo);
List<Sale> salesList = List.of(
new Sale(sneha, LocalDate.of(2020, Month.NOVEMBER, 13), 11034.20),
new Sale(raj, LocalDate.of(2020, Month.NOVEMBER, 20), 8234.23),
new Sale(florence, LocalDate.of(2020, Month.NOVEMBER, 19), 10003.67),
// …
new Sale(leo, LocalDate.of(2020, Month.NOVEMBER, 4), 9645.34));
MerchantExample app = new MerchantExample();
List<Merchant> topMerchants =
app.findTopMerchants(salesList, merchantList, 2020, Month.NOVEMBER);
System.out.println("Top merchants: ");
topMerchants.stream().forEach(m -> System.out.println(m.name()));
}
}
Static Members of Inner Classes
Prior to Java SE 16, you could not declare an explicitly or implicitly static member in an inner class unless that member is a constant variable. This means that an inner class cannot declare a record class member because nested record classes are implicitly static.
In Java SE 16 and later, an inner class may declare members that are either explicitly or implicitly static, which includes record class members. The following example demonstrates this:
public class ContactList {
record Contact(String name, String number) { }
public static void main(String[] args) {
class Task implements Runnable {
// Record class member, implicitly static,
// declared in an inner class
Contact c;
public Task(Contact contact) {
c = contact;
}
public void run() {
System.out.println(c.name + “, ” + c.number);
}
}
List<Contact> contacts = List.of(
new Contact("Sneha", "555-1234"),
new Contact("Raj", "555-2345"));
contacts.stream()
.forEach(cont -> new Thread(new Task(cont)).start());
}
}
You can serialize and deserialize instances of record classes, but you can’t customize the process by providing writeObject, readObject, readObjectNoData, writeExternal, or readExternal methods. The components of a record class govern serialization, while the canonical constructor of a record class governs deserialization. See Serializable Records for more information and an extended example. See also the section Serialization of Records in the Java Object Serialization Specification.
APIs Related to Record Classes
The abstract
class java.lang.Record is the common superclass of all record classes.
You might get a compiler error if your source file imports a class named Record from a package other than java.lang
. A Java source file automatically imports all the types in the java.lang package though an implicit import java.lang.*;
statement. This includes the java.lang.Record class, regardless of whether preview features are enabled or disabled.
Consider the following class declaration of com.techshitanshu.myapp.Record
:
public class Record {
public String greeting;
public Record(String greeting) {
this.greeting = greeting;
}
}
org.example.MyappPackageExample
, imports com.
techshitanshu.myapp.Record
with a wildcard but doesn’t compile:package org.example;
techshitanshu.*;
import com.myapp.
public class MyappPackageExample {
public static void main(String[] args) {
Record r = new Record("Hello world!");
}
}
./org/techshitanshu/example/MyappPackageExample.java:6: error: reference to Record is ambiguous
techshitanshu.myapp.Record in com.myapp and class java.lang.Record in java.lang match
Record r = new Record("Hello world!");
^
both class com.
./org/
techshitanshu/example/MyappPackageExample.java:6: error: reference to Record is ambiguous
Record r = new Record("Hello world!");
techshitanshu.myapp.Record in com.myapp and class java.lang.Record in java.lang match
^
both class com.
Both Record
in the com.myapp
package and Record
in the java.lang
package are imported with a wildcard. Consequently, neither class takes precedence, and the compiler generates an error when it encounters the use of the simple name Record
.
To enable this example to compile, change the import
statement so that it imports the fully qualified name of Record
:
Note:
The introduction of classes in the java.lang package is rare but necessary from time to time, such as Enum in Java SE 5, Module in Java SE 9, and Record in Java SE 14.
The class java.lang.Class has two methods related to record classes:
- RecordComponent[] getRecordComponents(): Returns an array of java.lang.reflect.RecordComponent objects, which correspond to the record class’s components.
- boolean isRecord(): Similar to
isEnum()
except that it returnstrue
if the class was declared as a record class.
Serialization of Records
Records are serialized differently than ordinary serializable or externalizable objects. The serialized form of a record object is a sequence of values derived from the record components. The stream format of a record object is the same as that of an ordinary object in the stream. During deserialization, if the local class equivalent of the specified stream class descriptor is a record class, then first the stream fields are read and reconstructed to serve as the record’s component values; and second, a record object is created by invoking the record’s canonical constructor with the component values as arguments (or the default value for component’s type if a component value is absent from the stream).
Like other serializable or externalizable objects, record objects can function as the target of back references appearing subsequently in the serialization stream. However, a cycle in the graph where the record object is referred to, either directly or transitively, by one of its components, is not preserved. The record components are deserialized prior to the invocation of the record constructor, hence this limitation.
The process by which record objects are serialized or externalized cannot be customized; any class-specific writeObject
, readObject
, readObjectNoData
, writeExternal
, and readExternal
methods defined by record classes are ignored during serialization and deserialization. However, a substitute object to be serialized or a designate replacement may be specified, by the writeReplace
and readResolve
methods, respectively. Any serialPersistentFields
field declaration is ignored. Documenting serializable fields and data for record classes is unnecessary, since there is no variation in the serial form, other than whether a substitute or replacement object is used. The serialVersionUID
of a record class is 0L
unless explicitly declared. The requirement for matching serialVersionUID values is waived for record classes.