Java Records
Records
Java records were introduced in Java 14. Records are immutable classes that are used to simplify data classes. Records are immutable. Records extend the java.lang.Record class. An example of a data class is the following:
public final class Point {
private final int x;
private final int y;
public Point(final int x, final int y) {
this.x = x;
this.y = y;
}
public int x() {
return x;
}
public int y() {
return y;
}
@Override
public boolean equals(final Object object) {
// ...
}
@Override
public int hashCode() {
// ...
}
@Override
public String toString() {
// ...
}
}
This can be rewritten by using a record with the following:
public record Point(int x, int y) {
}
A record introduces a new concept in Java. The section after the record name is called a header section. This determines what code will be automatically generated. The header section determines what parameters are in the default constructor and what accessor methods and immutable instance variables are generated. A record also automatically generates an implementation for equals(), hashCode() and toString() methods.
What Isn't Supported in Records
Although records are just classes, they do come with some limitations.
- Inheritance: Records cannot be extended and are implicitly final.
- Instance Variables: Only class variables are supported. All instance variables must be declared in the header section.
- Final Modifier: Variables in the header section cannot be marked as final.
- Instance Initializers: Instance initializers are not supported, only static initializers.
- Native Methods: Native methods are not supported.
Custom Constructors
The default constructor in a record matches the same signature defined in the header section.
public record Point(int x, int y) {
public Point(final int x, final int y) {
this.x = x;
this.y = y;
}
}
Compact Constructor
Records introduce a new concept in Java of a compact constructor. If you are wanting to customize the default constructor, you can use a compact constructor instead.
public record Point(int x, int y) {
public Point {
if (x < 0 ) {
throw new RuntimeException("x must be positive");
}
if (y < 0) {
throw new RuntimeException("y must be positive");
}
}
}
With this approach, you do not have to list the constructor parameters. You will also notice that the instance variables x and y are not assigned in the constructor. Instance variables defined in the header section are implicitly set at the end of the compact constructor. You can still assign these variables in a compact constructor if you are wanting to provide a different value for them.
public record Point(int x, int y) {
private static final int DEFAULT = 0;
public Point {
if (x < 0) {
x = DEFAULT;
}
if (y < 0) {
y = DEFAULT;
}
}
}
Compact constructors are a less error-prone approach and should be used instead when customizing the default constructor.
Multiple Constructors
You can provide multiple constructors in records, although they come with a limitation. All constructors must call the default constructor or a constructor that calls the default constructor.
public record Point(int x, int y) {
private static final int DEFAULT = 0;
public Point {
if (x < 0) {
x = DEFAULT;
}
if (y < 0) {
y = DEFAULT;
}
}
public Point(final int x) {
this(x, DEFAULT);
}
public Point() {
this(DEFAULT);
}
}
Explicit Accessor Methods
Accessor methods are generated automatically, but if you want to customize one, you can do so by providing the same method signature in the record that would be generated.
public record Point(int x, int y) {
public int x() {
System.out.println(x);
return x;
}
public int y() {
System.out.println(y);
return y;
}
}
Class Variables
Although instance variables aren't supported, you can still use class variables and constants. Since you can use class variables, static initializers are also supported.
public record Point(int x, int y) {
// Constant
public static final Point ZERO_ZERO = new Point(0, 0);
// Class variable
private static int counter;
// Static initalizer
static {
counter = 0;
}
}
Methods
Methods can be defined just like in classes. Records support both static and non-static methods.
public record Name(
String first
String last) {
public String full() {
return first + " " + last;
}
public static void myStaticMethod() {
// ...
}
}
Interfaces
Interfaces can be used just like in any other class. One thing to note is that if the interface has a method that matches an accessor method that will be automatically generated, it doesn't have to be provided.
public interface Identifiable {
String id();
}
public record Person(
// Implements the id() method in Identifiable
String id,
String firstName,
String lastName) implements Identifiable {
}
Annotations
Annotations can be added to the instance variables in the header section.
@MyClassAnnotation
public record Point(
@MyFieldAnnotation int x,
@MyFieldAnnotation int y) {
}
This would be the equivalent in a regular class to the following:
@MyClassAnnotation
public final class Point {
@MyFieldAnnotation
private final int x;
@MyFieldAnnotation
private final int y;
// ...
}
Local Records
Records can be defined and used in methods. Unlike local classes, local records are implicitly static, so they can't access any other variables in the enclosed method.
public static void main(final String[] args) {
final var name = "Zero";
record Point(int x, int y) {
public String name() {
// Compiler error:
// non-static variable name cannot
// be referenced from a static context
return name;
}
}
final var point = new Point(1, 2);
// ...
}
Nested Records
Just like local records, nested records are implicitly marked as static, so they follow the same rules as nested classes. They cannot access methods and variables from the outer class like an inner class can. Nested records can be declared in a class with the following:
public class OuterClass {
public record NestedRecord(int x) {
}
}
Conclusion
Records are a powerful and easy way to write data classes. Most features in a class are supported in record types. They provide an implementation of hashCode(), equals(), and toString() methods, so you do not have to worry about incorrectly writing these methods or not updating them when a record changes.