4 min read

Java Records

Records allow you to write data classes with very little code.
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.

  1. Inheritance: Records cannot be extended and are implicitly final.
  2. Instance Variables: Only class variables are supported. All instance variables must be declared in the header section.
  3. Final Modifier: Variables in the header section cannot be marked as final.
  4. Instance Initializers: Instance initializers are not supported, only static initializers.
  5. 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.