

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?
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!
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
, - a constructor that sets both fields (the so-called “canonical constructor”),
- the 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, the above mentioned “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)
Extensibility of Records
We can extend records with additional constructors, static fields, and methods. More about this in the following sections.
Additional Constructors in Records
You can extend records with additional constructors, such as a default constructor 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:
public record Point(int x, int y) {
/** Default constructor */
public Point() {
x = 0; // Not allowed!
y = 0; // Not allowed!
}
/** Custom constructor */
public Point(int value) {
x = value; // Not allowed!
y = value; // Not allowed!
}
}
Code language: Java (java)
Static Fields in Records
Records can be extended by static fields (final and non-final). For example, we could extract the 0 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)
Apart from the fact that one would rather implement a counter as AtomicLong
or LongAdder
(which would then be final again and thus could not serve as an example for a non-final static field), only the call to the additional parameterless constructor is counted in the preceding example.
Can we also count the invocation of the canonical constructor (i.e., the automatically generated one)?
Overwriting a Record's Canonical Constructor
We can also implement the canonical constructor ourselves and execute additional code before or after assigning the record's fields – e.g., increase the instanceCounter
:
public record Point(int x, int y) {
private static int instanceCounter;
/** Canonical constructor */
public Point(int x, int y) {
this.x = x;
this.y = y;
synchronized (Point.class) {
instanceCounter++;
}
}
}
Code language: Java (java)
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)
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) {
private static int instanceCounter;
/** Compact constructor */
public Point {
synchronized (Point.class) {
instanceCounter++;
}
}
}
Code language: Java (java)
Attention: With this variant, the fields are assigned only at the end of the constructor. If you access them within the constructor, they still contain default values (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.
Methods in Records
Besides additional constructors and static fields, you can also define static and non-static methods, such as the following:
The following static method returns the value of the instance counter (to make Point
thread-safe, any access to instanceCounter
is synchronized – including the getter, to ensure that we don't get a value from the CPU core cache, for example, but the latest value from main memory).
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)
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.
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.
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, and equals()
, hashCode()
, and toString()
methods.
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.