Records in Java

Java Records

by Sven WoltmannDecember 8, 2021

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 and int y,
  • a constructor that sets both fields (the so-called "canonical constructor"),
  • the methods x() and y() for reading the fields,
  • an equals() method that evaluates two Point instances as equal if their x and y coordinates are equal,
  • a hashCode() method that returns the same hash code for two equal Point 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:

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

Yes, 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 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)

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.

Sven Woltmann
About the author
I'm a freelance software developer with more than two decades of experience in scalable Java enterprise applications. My focus is on optimizing complex algorithms and on advanced topics such as concurrency, the Java memory model, and garbage collection. Here on HappyCoders.eu, I want to help you become a better Java programmer. Read more about me here.

Leave a Reply

Your email address will not be published. Required fields are marked *

You might also like the following articles

Text Blocks in Java Java Text Blocks
Java substring Method Java substring() Method
Java 18 Features with Examples Java 18 Features (with Examples)
Sealed Classes in Java Sealed Classes in Java