Records are one of two major new features in Java 16 (the second is “Pattern Matching for instanceof”). In this article, you will learn:
- What are Java records, and why do we need them?
- How to implement and use records in Java?
- How can we extend a Java Record with additional functionality?
- What is important in the context of inheritance?
- What should you consider when serializing and deserializing records?
- Why do you need records if you can have their components generated by the IDE … or by Lombok?
Let's start with an example from the times before records…
Why Do We Need Records?
Let's say we want to create an immutable class Point
with x
and y
coordinates and everything needed to make sense of this class. We want to instantiate Point
objects, read their fields, and store them in sets or use them as keys in maps.
The result would be something like the following code:
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
Point that = (Point) obj;
return this.x == that.x && this.y == that.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Point[x=%d, y=%d]".formatted(x, y);
}
}
Code language: Java (java)
That's quite a bit of boilerplate code for the “a class with x and y values” requirement.
Those who wanted and were allowed to use Lombok in their projects had a clear advantage. Lombok can create constructors, getters, equals()
, hashCode()
, and toString()
methods automatically. It reduces the code to a few lines:
@AllArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class Point {
private final int x;
private final int y;
}
Code language: Java (java)
That is already much more comfortable. Lombok is mature and integrates seamlessly with almost any IDE. I have been using it happily for over ten years.
Since Java 16, however, it is possible to do it even shorter:
public record Point(int x, int y) {}
Code language: Java (java)
Using records, the original 22 lines – or the seven lines with Lombok – become only one line! This is not only shorter, but also more secure (see sections Java Record vs. Class and Java Records vs. Lombok).
Let's look at how exactly to write records and use them.
How to Implement and Use Records in Java?
In the previous section, we saw how to write a record with a single line of code:
public record Point(int x, int y) {}
Code language: Java (java)
From this one line of code, the compiler generates a class Point
with:
- the final fields
int x
andint y
(the so-called “components” of the record), - a constructor that sets both fields (the so-called “canonical constructor”),
- the accessor methods
x()
andy()
for reading the fields, - an
equals()
method that evaluates twoPoint
instances as equal if theirx
andy
coordinates are equal, - a
hashCode()
method that returns the same hash code for two equalPoint
instances (in the example, the hash code is calculated as x * 31 + y), - a
toString()
method that returns a human-readable text (in the example “Point[x=…, y=…]”).
You can use Point
like any regular class (in the first line of the following code example, the above mentioned, automatically generated “canonical constructor” is called):
Point p = new Point(5, 10);
int x = p.x();
int y = p.y();
Code language: Java (java)
You can compare two points, for example, as follows:
Point p1 = new Point(8, 4);
Point p2 = new Point(4, 3);
if (p1.equals(p2)) {
// ...
}
Code language: Java (java)
Java Record Constructors
In the previous section, you learned that the compiler automatically creates a constructor, the so-called canonical constructor. In this chapter, you will learn how you can overwrite this canonical constructor, how you can write so-called “compact” constructor – and any other non-canonical constructors.
Overwriting a Record's Canonical Constructor
We can implement the canonical constructor of a record ourselves:
public record Point(int x, int y) {
/** Canonical constructor as the compiler would generate it */
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
Code language: Java (java)
However, this only makes sense if we execute additional code before or after assigning the record fields – for example, we might want to ensure that the coordinates are not negative:
public record Point(int x, int y) {
/** Canonical constructor */
public Point(int x, int y) {
if (x < 0 || y < 0) throw new IllegalArgumentException();
this.x = x;
this.y = y;
}
}
Code language: Java (java)
In addition to validation, we could also transform parameters or, for example, create a defensive copy of an array.
With this form, the constructor's signature must be exactly the same as the record's. The following, in contrast, is not allowed:
public record Point(int x, int y) {
public Point(int a, int b) { // Other names than x and y are not allowed!
this.x = a;
this.y = b;
}
}
Code language: Java (java)
The compiler would reject this with the following error message:
$ javac Point.java
Point.java:4: error: invalid canonical constructor in record Point
public Point(int a, int b) {
^
(invalid parameter names in canonical constructor)
1 error
Code language: plaintext (plaintext)
It is equally important that all fields are set (logically, they are final). If we were to set only x
but not y
, the compiler would abort with the following message:
$ javac Point.java
Point.java:4: error: variable y might not have been initialized
}
^
1 error
Code language: plaintext (plaintext)
Interestingly, however, there is no need for a 1:1 mapping of the parameters to the fields. You don't even have to use all parameters. So the following code is also valid:
public record Point(int x, int y) {
public Point(int x, int y) {
this.x = x;
this.y = x; // Assigning this.y to x here - and ignoring y
}
}
Code language: Java (java)
Fortunately, modern IDEs recognize this. IntelliJ, for example, warns that “'x' should probably not be assigned to 'y'”.
Finally, the visibility of the canonical constructor must not be more restrictive than the visibility of the record itself. This means that a record marked as private may have a constructor marked as public – but a record declared as public may not have a private constructor – the following is, therefore, not allowed:
public record Point(int x, int y) {
private Point(int x, int y) { // private constructor not allowed for public record
this.x = x;
this.y = y;
}
}
Code language: Java (java)
The compiler would abort with the following error message:
$ javac Point.java
Point.java:2: error: invalid canonical constructor in record Point
private Point(int x, int y) {
^
(attempting to assign stronger access privileges; was public)
1 error
Code language: plaintext (plaintext)
Compact Constructor
There is another more concise variant to override the canonical constructor. You can omit the parameters in the signature and the assignments completely. We call this type a “compact constructor”:
public record Point(int x, int y) {
/** Compact constructor */
public Point { // ← No parameters here
if (x < 0 || y < 0) throw new IllegalArgumentException();
// ← No assignments here
}
}
Code language: Java (java)
The compiler automatically inserts the assignments this.x = x
and this.y = y
at the end of the constructor and thus ultimately generates exactly the same bytecode from the compact constructor as from the canonical constructor shown second in the previous section.
The parameters may also be changed within the constructor, e.g., we could tacitly replace all negative values with 0:
public record Point(int x, int y) {
/** Compact constructor */
public Point {
x = Math.max(x, 0);
y = Math.max(y, 0);
}
}
Code language: Java (java)
This would correspond to the following canonical constructor:
public record Point(int x, int y) {
/** Canonical constructor */
public Point(int x, int y) {
x = Math.max(x, 0);
y = Math.max(y, 0);
this.x = x;
this.y = y;
}
}
Code language: Java (java)
Both forms of the constructor are ultimately the same, and only either a canonical or a compact constructor may be implemented.
My recommendation is to always use a compact constructor. After all, we programmers want to express our ideas – and not write unnecessary boilerplate code.
Caution: As the record's components are only set at the end of the constructor, you should not access the accessor methods – x()
and y()
in the example – within the constructor. The components are still assigned default values at this point (i.e., 0 in the case of int
).
You should access the (not explicitly specified) constructor parameters instead:
public record Point(int x, int y) {
/** Compact constructor */
public Point {
System.out.println(x()); // Prints 0 (fields are not yet assigned)
System.out.println(x); // Prints the x parameter passed to the constructor
}
}
Code language: Java (java)
This becomes evident when you try to access the fields within the constructor using this
:
public record Point(int x, int y) {
/** Compact constructor */
public Point {
System.out.println(this.x); // Not allowed - x is not yet initialized
}
}
Code language: Java (java)
This code leads to a compiler error:
$ javac Point.java
Point.java:3: error: variable x might not have been initialized
System.out.println(this.x);
^
1 error
Code language: plaintext (plaintext)
It would have been consistent not to allow access via x()
in the constructor either.
Additional Constructors in Records
You can extend records with additional constructors, such as a default constructor (one without parameters) or one that sets x
and y
to the same value:
public record Point(int x, int y) {
/** Default constructor */
public Point() {
this(0, 0);
}
/** Custom constructor */
public Point(int value) {
this(value, value);
}
}
Code language: Java (java)
As shown in the previous example, you must always a) set all fields and b) do this by delegating to the canonical (i.e., the automatically generated) constructor via this(…)
.
Setting the values directly, as in the following code, is not allowed in these additional constructors:
public record Point(int x, int y) {
/** Default constructor */
public Point() {
this.x = 0; // Not allowed!
this.y = 0; // Not allowed!
}
/** Custom constructor */
public Point(int value) {
this.x = value; // Not allowed!
this.y = value; // Not allowed!
}
}
Code language: Java (java)
The reason for this is that no matter which constructor is used, the parameter validations that may be implemented in the canonical or compact constructor should always be called.
Static Fields in Records
Records can be extended by static fields (final and non-final). For example, we could extract the 0 from the default constructor shown above into a constant:
public record Point(int x, int y) {
private static final int ZERO = 0;
public Point() {
this(ZERO, ZERO);
}
}
Code language: Java (java)
We could also add a static instance counter that is incremented in the constructor:
public record Point(int x, int y) {
private static final int ZERO = 0;
private static long instanceCounter = 0;
public Point() {
this(ZERO, ZERO);
synchronized (Point.class) {
instanceCounter++;
}
}
}
Code language: Java (java)
In fact, I would rather implement such a counter as AtomicLong
or LongAdder
– but then they would be final again and therefore not suitable as an example for a non-final static field. ;-)
Methods in Records
Just like the canonical constructor, we can also override the automatically generated accessor methods of a record. The following record contains an array component and creates a defensive copy of the array in the constructor and in the accessor in order to prevent changes to the array stored in the record:
public record ImmutableArrayHolder(int[] array) {
/* Compact constructor */
public ImmutableArrayHolder {
array = array.clone();
}
/* Accessor method */
public int[] array() {
return array.clone();
}
}
Code language: Java (java)
Besides additional constructors and static fields, you can also define additional static and non-static methods in Java records.
The following static method returns the value of the instance counter:
public record Point(int x, int y) {
private static long instanceCounter = 0;
// ... Constructor(s) increasing instanceCounter ...
public static synchronized long getInstanceCounter() {
return instanceCounter;
}
}
Code language: Java (java)
We could implement a non-static method, for example, to calculate the Euclidean distance to another point:
public record Point(int x, int y) {
public double distanceTo(Point target) {
int dx = target.x() - this.x();
int dy = target.y() - this.y();
return Math.sqrt(dx * dx + dy * dy);
}
}
Code language: Java (java)
We can call the method, for example, as follows:
Point p1 = new Point(17, 3);
Point p2 = new Point(18, 12);
double distance = p1.distanceTo(p2);
Code language: Java (java)
When implementing and invoking record methods, there is no difference from normal classes.
Records and Inheritance
Records can implement interfaces:
public interface WithXCoordinate {
int x();
}
public record Point(int x, int y) implements WithXCoordinate {}
Code language: Java (java)
This is also possible in combination with sealed types released in Java 17:
public interface WithXCoordinate permits Point, Point3D {
int x();
}
public record Point(int x, int y) implements WithXCoordinate {}
public record Point(int x, int y, int z) implements WithXCoordinate {}
Code language: Java (java)
Records cannot, however, inherit from classes. So the following is not allowed:
public class TaggedElement {
private String tag;
}
public record Point(int x, int y) extends TaggedElement {} // Not allowed!
Code language: Java (java)
This is because records already inherit from the java.lang.Record class – and they are supposed to be immutable. And that they would not be if they inherited from a mutable class.
Records are implicitly final, so you can't inherit from them either. The following code is also invalid:
public record Point(int x, int y) {}
public class TaggedPoint extends Point { // Not allowed!
private String tag;
TaggedPoint(int x, int y, String tag) {
super(x, y);
this.tag = tag;
}
}
Code language: Java (java)
Characteristics of Records
Compared to regular classes, you should know some peculiarities about records. I will explain these in the following sections.
Local Records
Records may also be defined locally (i.e., within methods). This can be especially helpful if you want to store intermediate results consisting of several related variables.
In the following example, we define, within the findFurthestPoint()
method, the local record PointWithDistance
: a combination of a Point
and a double
value representing the distance of the point to an origin point.
With the help of the local record, we fill a list of points and their distances to the current point. From this list, we then determine the PointWithDistance
with the greatest distance – to then extract the corresponding Point
from it.
public Point findFurthestPoint(Point origin, Point... points) {
record PointWithDistance(Point point, double distance) {}
List<PointWithDistance> pointsWithDistance = new ArrayList<>();
for (Point point : points) {
double distance = origin.distanceTo(point);
pointsWithDistance.add(new PointWithDistance(point, distance));
}
PointWithDistance furthestPointWithDistance = Collections.max(
pointsWithDistance,
Comparator.comparing(PointWithDistance::distance));
return furthestPointWithDistance.point();
}
Code language: Java (java)
Records within Inner Classes
Records may also be defined within inner classes:
class OuterClass {
// ...
class InnerClass {
record InnerClassRecord(String foo, int bar) {}
// ...
}
}
Code language: Java (java)
This possibility is worth mentioning in that it was only made possible with the final release of records by JDK Enhancement Proposal 395.
Records and Reflection
You can easily change the final fields of regular classes using Reflection. In the following code, Point
is the class from the beginning of this article:
Point point = new Point(10, 5);
System.out.println("point = " + point);
Field xField = Point.class.getDeclaredField("x");
xField.setAccessible(true);
System.out.println("point.x = " + xField.get(point));
xField.set(point, 55);
System.out.println("point = " + point);
Code language: Java (java)
The code outputs the following:
point = Point[x=10, y=5]
point.x = 10
point = Point[x=55, y=5]
Code language: plaintext (plaintext)
That means that we have read and changed the actually private and final x
field of the Point
class via reflection!
If we call the same code with the Point
record, we get the following output:
point = Point[x=10, y=5]
point.x = 10
Exception in thread "main" java.lang.IllegalAccessException:
Can not set final int field eu.happycoders.records.Point.x to java.lang.Integer
Code language: plaintext (plaintext)
We can thus also read private fields from records via reflection (and thus, for example, bypass an accessor that creates a defensive copy of a mutable component, such as an array).
However, unlike classes, records are protected from changes by reflection.
Deserializing Records
Records have a particularity when deserializing them. I will show this with the following example.
Let's first extend the Point
constructor with a parameter validation. Let's say we want to exclude negative values. We use the compact notation for overriding the canonical constructor. We also make the class serializable:
public record Point(int x, int y) implements Serializable {
@Serial private static final long serialVersionUID = -1482007299343243215L;
public Point {
if (x < 0) throw new IllegalArgumentException("x must be >= 0");
if (y < 0) throw new IllegalArgumentException("y must be >= 0");
}
}
Code language: Java (java)
To see the difference in deserialization, we create an analogous regular class PointClass
:
public final class PointClass implements Serializable {
@Serial private static final long serialVersionUID = 8411630734446201523L;
private final int x;
private final int y;
public Point(int x, int y) {
if (x < 0) throw new IllegalArgumentException("x must be >= 0");
if (y < 0) throw new IllegalArgumentException("y must be >= 0");
this.x = x;
this.y = y;
}
// ... getters, equals(), hashCode(), toString() ...
}
Code language: Java (java)
We temporarily comment out the parameter validation and serialize an invalid Point
record and an invalid PointClass
class to a file each, using the following code:
PointClass pc = new PointClass(-5, 5); // Parameter validation temporarily commented
try (FileOutputStream fileOut = new FileOutputStream("point-class.bin");
ObjectOutputStream objectOut = new ObjectOutputStream(fileOut)) {
objectOut.writeObject(pc);
}
Point p = new Point(-5, 5); // Parameter validation temporarily commented
try (FileOutputStream fileOut = new FileOutputStream("point-record.bin");
ObjectOutputStream objectOut = new ObjectOutputStream(fileOut)) {
objectOut.writeObject(p);
}
Code language: Java (java)
After that, we uncomment the parameter validation and try to deserialize the serialized objects:
try (FileInputStream fileIn = new FileInputStream("point-class.bin");
ObjectInputStream objectIn = new ObjectInputStream(fileIn)) {
PointClass pointClass = (PointClass) objectIn.readObject();
System.out.println("pointClass = " + pointClass);
}
try (FileInputStream fileIn = new FileInputStream("point-record.bin");
ObjectInputStream objectIn = new ObjectInputStream(fileIn)) {
Point point = (Point) objectIn.readObject();
System.out.println("point = " + point);
}
Code language: Java (java)
The result is surprising and reveals another difference between records and classes:
pointClass = PointClass{x=-5, y=5}
Exception in thread "main" java.io.InvalidObjectException: x must be >= 0
at ...
Caused by: java.lang.IllegalArgumentException: x must be >= 0
at records.Point.<init>(Point.java:10)
Code language: plaintext (plaintext)
The faulty class can be deserialized without any problems – the record, however, cannot.
The reason: When deserializing a class, the values read from the ObjectInputStream
are written directly into the fields of the class. In the case of a record, on the other hand, the canonical constructor is called – and any parameter validations it contains are executed.
Java Record vs. Class
I am often asked why you need records when you can simply have an IDE generate the constructor, getter, equals()
, hashCode()
, and toString()
.
Firstly, in my opinion, it is not the task of an IDE to compensate for the shortcomings of a programming language. A programming language should be so sophisticated that we can express our ideas with as little code as possible – the compiler should do the rest. Not everyone uses the same IDE, and different IDEs generate different source code.
Records have other concrete advantages:
- Record components are truly immutable. While final fields of a regular class can be changed via reflection, this is not possible with records (see section Records and Reflection).
- It is not possible to create invalid records via deserialization, as the canonical constructor of the record is called during deserialization (in contrast to classes).
- For the
equals()
,hashCode()
, andtoString()
methods, the compiler generates a special bytecode that calls implementations of these methods in the JVM. This means that these methods can be further optimized in future Java versions without having to recompile existing code. - Records work closely with other language features, such as Records Patterns finalized in Java 21.
Java Records vs. Lombok
Similarly, I am often asked about the advantages of records over Lombok.
In my opinion, we should not hand over responsibility for something that a language should be able to do to a library. Because there are risks involved: What if the library is no longer maintained? What if it is not adapted to new Java versions or even completely discontinued?
In addition, the same concrete disadvantages apply as with regular classes: Final fields of classes can be changed via reflection; invalid instances can be created via deserialization; equals()
, hashCode()
, and toString()
cannot be optimized by the JVM; and pattern matching does not work with Lombok-annotated classes.
Summary
Records provide a compact notation to define classes with only final fields. Records automatically include a constructor that sets all final fields (the canonical constructor), read access methods for all fields, as well as equals()
, hashCode()
, and toString()
methods optimized by the JVM.
Records can be extended by additional constructors, static fields, and static as well as non-static methods. The canonical constructor can be overridden.
Records can implement interfaces (including sealed ones) but cannot extend classes, nor can they be inherited from.
When deserializing records, their canonical constructor is invoked – and any parameter validations it may contain.
Records were released as preview features in Java 14 and Java 15 and were declared production-ready in Java 16 through JDK Enhancement Proposal 395. Records were developed as part of Project Amber, which also includes switch expressions, text blocks, pattern matching, and sealed classes.
If you liked the article, feel free to share it using one of the share buttons at the end or leave a comment. If you want to be informed when the next article is published on HappyCoders.eu, click here to sign up for the HappyCoders newsletter.