9 Record Classes
Record classes, which are a special kind of class, help to model plain data aggregates with less ceremony than normal classes.
For background information about record classes, see JEP 395.
A record declaration specifies in a header a description of its contents; the
appropriate accessors, constructor, equals
, hashCode
,
and toString
methods are created automatically. A record's fields are
final because the class is intended to serve as a simple "data carrier".
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);
To access a record's component fields, call its accessor methods:
System.out.println("Length: " + r.length() + ", width: " + r.width());
This example prints the following output:
Length: 4.0, width: 5.0
Topics
- The Canonical Constructor of a Record Class
- Alternative Record Constructors
- Explicit Declaration of Record Class Members
- Features of Record Classes
- Record Classes and Sealed Classes and Interfaces
- Local Record Classes
- Static Members of Inner Classes
- unresolvable-reference.html#GUID-36453338-80AA-4E30-B3A0-6A5F88E65733
- Differences Between the Serialization of Records and Ordinary Objects
- APIs Related to Record Classes
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.
Alternative Record Constructors
You can define alternative, noncanonical constructors whose argument list
doesn't match the record's type parameters. However, these constructors must invoke the
record's canonical constructor. In the following example, the constructor for the record
RectanglePair
contains one parameter, a
Pair<Float>
. It calls its implicitly defined canonical
constructor to initialize its fields length
and
width
:
record Pair<T extends Number>(T x, T y) { }
record RectanglePair(double length, double width) {
public RectanglePair(Pair<Double> corner) {
this(corner.x().doubleValue(), corner.y().doubleValue());
}
}
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 Record Classes as Permitted Subclasses for an example.
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()));
}
}
Like nested record classes, local record classes are implicitly static, which means that their own methods can't access any variables of the enclosing method, unlike local classes, which are never static.
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());
}
}
Differences Between the Serialization of Records and Ordinary Objects
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.
Records are serialized differently than ordinary serializable objects. The serialized form of a record object is a sequence of values derived from the record components. When a record object is deserialized, its component values are reconstructed from this sequence of values. Afterward, a record object is created by invoking the record's canonical constructor with the component values as arguments. In contrast, when an ordinary object is deserialized, it’s possible that it can be created without one of its constructors being invoked. Consequently, serializable records leverage the guarantees provided by record classes to offer a simpler and more secure serialization model.
Record Serialization Principles
Two principles exist with regards to record serialization:
- Serialization of a record object is based only on its state components.
- Deserialization of a record object uses only the canonical constructor.
A consequence of the first point is that customization of the serialized form of a record object is not possible — the serialized form is based on the state components and the state components only. As a result of this restriction, programmers more easily understand the serialized form of a record: it consists of the state components of the record.
The second point relates to the mechanics of the deserialization process. Suppose deserialization is reading the bytes of an object for a normal class (not a record class). Deserialization would create a new object by invoking the no-args constructor of a superclass, then use reflection to set the object’s fields to values deserialized from the stream. This is insecure because the normal class has no opportunity to validate the values coming from the stream. The result may be an “impossible” object that could never be created by an ordinary Java program using constructors. With records, deserialization works differently. Deserialization creates a new record object by invoking a record class’s canonical constructor, passing values deserialized from the stream as arguments to the canonical constructor. This is secure because it means the record class can validate the values before assigning them to fields, just like when an ordinary Java program creates a record object via new. “Impossible” objects are impossible. This is achievable because the record components, the canonical constructor, and the serialized form are all known and consistent.
How Record Serialization Works
To demonstrate the differences between the serialization of ordinary objects
and records, let’s create two classes, RangeClass
and
RangeRecord
, that model a range of integers.
RangeClass
is an ordinary class while RangeRecord
is a
record. They have a low-end value, lo
, and a high-end value,
hi
. They also implement Serializable
because we want
to be able to serialize instances of them.
public class RangeClass implements Serializable {
private static final long serialVersionUID = -3305276997530613807L;
private final int lo;
private final int hi;
public RangeClass(int lo, int hi) {
this.lo = lo;
this.hi = hi;
}
public int lo() { return lo; }
public int hi() { return hi; }
@Override public boolean equals(Object other) {
if (other instanceof RangeClass that
&& this.lo == that.lo && this.hi == that.hi) {
return true;
}
return false;
}
@Override public int hashCode() {
return Objects.hash(lo, hi);
}
@Override public String toString() {
return String.format("%s[lo=%d, hi=%d]", getClass().getName(), lo, hi);
}
}
Note the verbose boilerplate code for the equals
,
hashCode
, and toString
methods.
The following example is the equivalent record counterpart of
RangeClass
. You make a record class serializable in the same way as a
normal class, by implementing Serializable
.
public record RangeRecord (int lo, int hi) implements Serializable { }
Note that you don’t have to add any additional boilerplate to
RangeRecord
to make it serializable. Specifically, you don’t need
to add a serialVersionUID
field because the
serialVersionUID
of a record class is 0L
unless
explicitly declared, and the requirement for matching the serialVersionUID value is
waived for record classes.
Rarely, for migration compatibility between normal classes and record classes, a
serialVersionUID
may be declared. See the section 5.6.2 Compatible Changes in Java Object Serialization Specification for
more information.
The following examples serialize a RangeClass
object and
RangeRecord
object, both with the same high-end and low-end
values.
import java.io.*;
public class Serialize {
public static void main(String... args) throws Exception {
try (var fos = new FileOutputStream("serial.data");
var oos = new ObjectOutputStream(fos)) {
oos.writeObject(new RangeClass(100, 1));
oos.writeObject(new RangeRecord(100, 1));
}
}
}
import java.io.*;
public class Deserialize {
public static void main(String... args) throws Exception {
try (var fis = new FileInputStream("serial.data");
var ois = new ObjectInputStream(fis)) {
System.out.println(ois.readObject());
System.out.println(ois.readObject());
}
}
}
Running these two examples print the following output:
RangeClass[lo=100, hi=1]
RangeRecord[lo=100, hi=1]
Oops! Did you manage to spot the mistake? The low-end value is higher than the high-end value. This shouldn’t be allowed. An integer range implementation should have this invariant: the low end of the range can be no higher than the high end.
Let's modify RangeClass
and RangeRecord
as follows to
add this invariant:
public class RangeClass implements ... {
// ...
public RangeClass(int lo, int hi) {
if (lo > hi)
throw new IllegalArgumentException(String.format("%d, %d", lo, hi));
this.lo = lo;
this.hi = hi;
}
// ..
}
public record RangeRecord (int lo, int hi) implements ... {
public RangeRecord {
if (lo > hi)
throw new IllegalArgumentException(String.format("%d, %d", lo, hi));
}
}
Note that RangeRecord
uses the compact version of the canonical
constructor declaration, which enables you to omit the boilerplate assignments.
With the updated RangeClass
and RangeRecord
examples,
the Deserialize
example prints output similar to the following:
RangeClass[lo=100, hi=1]
Exception in thread "main" java.io.InvalidObjectException: 100, 1
at java.base/java.io.ObjectInputStream.readRecord(ObjectInputStream.java:2296)
at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2183)
at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1685)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:499)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:457)
at Deserialize.main(Deserialize.java:9)
Caused by: java.lang.IllegalArgumentException: 100, 1
at RangeRecord.<init>(RangeRecord.java:6)
at java.base/java.io.ObjectInputStream.readRecord(ObjectInputStream.java:2294)
... 5 more
The example attempts to deserialize the stream objects in the
serial.data
file even though RangeClass
and
RangeRecord
have been created with a low-end value of 100 and a
high-end value of 1.
The example demonstrates that a normal class and a record class are deserialized differently:
- A
RangeClass
object was deserialized even though the newly created object violates the constructor invariant. This may seem counterintuitive at first, but as described earlier, deserialization of an object whose class is a normal class (not a record class) creates the object by invoking the no-args constructor of the (first non-serializable) superclass, which in this case is java.lang.Object. Of course, it would not be possible for the exampleSerialize
to generate such a byte stream for aRangeClass
object because the example must use the two-arg constructor with its invariant checking. However, remember deserialization operates on just a stream of bytes, and these bytes can, in some cases, come from almost anywhere. - However, the
RangeRecord
stream object failed to deserialize because its stream field values for the low end and high end violate the invariant check in the constructor. This is nice, and actually what we want: deserialization proceeds through the canonical constructor.
The fact that a serializable class can have a new object created without one of its constructors being invoked is often overlooked, even by experienced developers. An object created by invoking a distant no-args constructor can lead to unexpected behavior at run time, since invariant checks in the deserialized class’s constructor are not performed. However, deserialization of a record object cannot be exploited to create an “impossible” object.
See the section 1.13 Serialization of Records in Java Object Serialization Specification for more information.
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.
com.myapp.Record
:package com.myapp;
public class Record {
public String greeting;
public Record(String greeting) {
this.greeting = greeting;
}
}
The following example, org.example.MyappPackageExample
,
imports com.myapp.Record
with a wildcard but doesn't compile:
package org.example;
import com.myapp.*;
public class MyappPackageExample {
public static void main(String[] args) {
Record r = new Record("Hello world!");
}
}
The compiler generates an error message similar to the following:
./org/example/MyappPackageExample.java:6: error: reference to Record is ambiguous
Record r = new Record("Hello world!");
^
both class com.myapp.Record in com.myapp and class java.lang.Record in java.lang match
./org/example/MyappPackageExample.java:6: error: reference to Record is ambiguous
Record r = new Record("Hello world!");
^
both class com.myapp.Record in com.myapp and class java.lang.Record in java.lang match
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
:
import com.myapp.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.