Java Records
Immutable, Simple, Clean
This article is just aimed to be an introduction to the upcoming JDK feature called "Records". They are sometimes called "Record Classes". Records were first available as a preview feature in JDK 14 ( JEP359), returned for a second preview in JDK 15 ( JEP384), and is expected to have its be released in JDK 16 ( JEP395).
The goal of Records are to provide a minimal way to declare immutable classes. It allows declaration of types
with a canonical set of values, while implementing all the obvious boilerplate code (accessors, hashCode
,
equals
, toString
) automatically.
Specifically the main goals described by the JEP are:
- 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.
record Coordinate(int x, int y) {}
This article was written using the following versions:
Java | 15 |
---|
Making Immutable Classes
With Java currently, in order to create classes for which instances are immutable, we have to implement a lot of the standard boilerplate as for mutable classes. We make fields final, initialized in the constructor and do not provide mutator methods.
public class MinMax {
private final int min;
private final int max;
public MinMax(final int min, final int max) {
this.min = min;
this.max = max;
}
public int min() {
return min;
}
public int max() {
return max;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final MinMax other = (MinMax) o;
return min == other.min && max == other.max;
}
@Override
public int hashCode() {
return Objects.hash(min, max);
}
@Override
public String toString() {
return "MinMaxOld{min=" + min + ", max=" + max + '}';
}
}
Having to do this for every class is problematic for a number reasons, including:
- It's a waste of time to write. This is standard boilerplate, that most IDEs generate anyway
- It's a waste of time to read
- In order to tell if this class is just using standard implementations for boilerplate methods, readers must read over the whole class. Only to find it's standard boilerplate.
Enter Records
Below is the equivalent using the new record
feature. It allows us to create clean, single
purpose classes with a clear set of component values.
record MinMax(int min, int max) {}
Above we've created a Record with min and max integer components. With this Record, we automatically get a standard set of implementations:
- Canonical Constructor
- Accessor methods (
min()
andmax()
) for each component equals()
hashCode()
toString()
A record is therefore safe to use in collections.
hashCode
, and relative behavior of equals
.
Some other notes about Records:
- They can implement interfaces
- You can declare static and non-static methods in records.
- You can declare other constructors, but all constructors must call the canonical constructor. This ensures a record instance is always constructed in a canonical/standard way
- Instance fields cannot be specified apart from those in the Record header
-
Records are implicitly classes inheriting the
Record
abstract class. - Records can be created in local scopes (inside methods) when needed temporarily.
- Record definitions nested in other classes/records are implicitly static.
I recommend reading the JEP for a full explanation about Records.
Enabling Preview Features
Because Records are not a release feature yet, only expect in JDK 16, usage needs to be explicitly enabled.
You can enable preview features when compiling by using the --enable-preview
flag:
javac --source 15 --enable-preview RecordExample.java
Equivalently, when running the Java application, you need to enable preview features:
java --enable-preview RecordExample
With Gradle
If you're using Gradle to build, or run tests you can enable them as follows
// Compile with preview features
tasks.withType(JavaCompile) {
options.compilerArgs += '--enable-preview'
}
// Run Tests with Preview features enabled
test {
useJUnitPlatform()
jvmArgs(['--enable-preview'])
}
Usage Examples
This isn't rocket science, but for completeness here are some examples that show benefits from the simplicity of Records.
Multiple Returns
In other languages like Python or Ruby you can easily create tuple, list or array literals. This allows functions to easily return multiple items without much hassle. In Java there are no type-agnostic lists or tuples, and creating classes just for returning something seems overkill.
Records really come to the rescue here, as we can easily create composites of data in order to return multiple items. And even better than nameless, typeless tuple values in Python or Ruby, with Records we have meaning and type safety.
record SearchResult(Person person, int index) {}
static Optional<SearchResult> findByFirstName(List<Person> people, String firstname) {
int i = 0;
for (Person person : people) {
if (firstname.equals(person.firstName)) {
return Optional.of(new SearchResult(person, i));
}
i++;
}
return Optional.empty();
}
Here we create a nested Record to allow for returning the matched Person along with the index at which it was found.
In Collections
Because of the auto-generated equals()
and hashCode()
methods, Records make it really
easy to make temporary value types for inserting into collections like Sets or Maps.
For example, when implementing a BFS search on a grid we could create local Records in order to store our
cells and also cells with distance. We can easily just use them in the Set and trust the
hashCode
and equals
to work.
static int bfsShortestPath(final char[][] map, int startX, int startY) {
record Cell(int x, int y) {}
record CellDist(Cell cell, int distance) {}
ArrayDeque<CellDist> queue = new ArrayDeque<>();
Set<Cell> visited = new HashSet<>();
queue.offer(new CellDist(new Cell(startX, startY), 0));
while(!queue.isEmpty()) {
CellDist current = queue.poll();
if (visited.contains(current)) continue;
visited.add(current.cell);
if (map[current.cell.y][current.cell.x] == 'X') {
return current.distance;
}
if (/*in bounds*/) {
queue.offer(new CellDist(new Cell(current.cell.x + 1, current.cell.y),
distance + 1
));
}
//...other neighbours
}
return -1;
}
Also note above how the Records are defined locally within the scope of that method. And because of their
succinct form, provide a lot of convenience without adding too much clutter. The visited
Set above
is used to track which Cell
s have been processed already. The record CellDist
provides a way to compose Cell
and the distance from the start.
More To Come
Note that the below functionality is not available even in preview yet, but it should be coming in some or other form, and it's quite elegant! Java is improving fast ; it's exciting!
I'm not in any way affiliated with the JDK team so take these ponderings with a grain of salt!
Deconstructing
Records are still to come out in their first version. In the Inside Java Podcast about Records with Gavin Bierman I heard something quite exciting about the future of Records and another Preview feature "Pattern Matching", which will allow pattern-matching and record component deconstruction in one line, within a limited scope. I would imagine it to look something like this:
if (shape instanceof Rectangle(Point(var x, var y), Point(var u, var v))) {
System.out.println("Is a Rectangle with width: " + Math.abs(u-x));
}
Exhaustive switch statements
Reading this article on Pattern Matching there's a mention of the combination of 3 new features (Records, Pattern-Matching and Sealed classes) that will allow switch statements to exhaustively switch on a type without missing subtypes:
sealed interface Shape permits Rectangle, Circle { }
record Rectangle(int left, int right, int top, int bottom) implements Shape {}
record Circle(int radius, Point center) implements Shape {}
...
Point center(Shape shape) {
return switch(shape) {
case Rectangle(int left, int right, int top, int bottom): return new Point((right-left)/2, (bottom-top)/2);
case Circle(int radius, Point center): return center;
}
}
At compile-time, it can be confirmed whether your cases exhaust all the subtypes and we can also deconstruct each subtype respective to their canonical component definitions.
Summary
In the above we learned the following:
- How Records will be coming to the JDK to provide an easy, standard way to create immutable classes
- How to try out Records as preview features in JDK 14, 15
- That very usable value classes can be created with concise syntax
- A little about what else might be coming in future improvements to Records in conjunction with other features like Pattern Matching
References
Bitcoin

Zap me some sats


This blog post itself is licensed under a Creative Commons Attribution 4.0 International License. However, the code itself may be adapted and used freely.