Learnitweb

Record classes in Java

1. Introduction

Many developers believe that the Java is too verbose. Writing a simple data-carrier immutable class involves a lot of low-value code such as constructors, accessors, equals, hashCode, toString etc. For example, a class Point representing a point in two dimensional space with two coordinates x and y can be written like this:

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 o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point{" +
                "x=" + x +
                ", y=" + y +
                '}';
    }
}

Here, hashCode() and equals() are two important methods. If you try to omit these then this may result in strange behavior and issues. IDEs help in writing this boilerplate code but code is still there.

A record in Java is a class that acts as a carrier of immutable data. Records were introduced in JDK 14 as a preview feature. Records were still a preview feature in JDK 15. Records were made final in JDK 16. Records acts as a simple aggregation of values. Records automatically implement data-driven methods such as accessors and equals.

2. Declaration

A record class declaration consists of a name, optional type parameters, a header, and a body. Declaration of a records requires you to declare the state of the record. The header lists the components of the record class, which are the variables forming its state. For example:

record Point(int x, int y) { }

A record class automatically gets many standard members automatically:

  • For each component listed in the header, the record class includes two members: a public accessor method with the same name and return type as the component, and a private final field of the same type as the component.
  • A canonical constructor with a signature matching the header is included, assigning each private field to the corresponding argument from a new expression that instantiates the record.
  • The equals and hashCode methods which ensure that two record instances are considered equal if they are of the same type and have identical component values.
  • A toString method that provides a string representation of all the record components, including their names.

So when you declare the state of the record, an API is committed which involved the constructor, accessor, hashCode and equals.

3. Important points about Record

  • A new instance of a record class is created using new expression.
  • A record class can be declared top level or nested, and can be generic.
  • A record class can declare static methods, fields, and initializers.
  • A record class can declare instance methods.
  • A record class can implement interfaces. A record class can not specify superclass but can implement interfaces and declare instance methods to implement them.
  • If a record class is itself nested, then it is implicitly static; this avoids an immediately enclosing instance which would silently add state to the record class.
  • A record class, and the components in its header, may be decorated with annotations. Any annotations on the record components are propagated to the automatically derived fields, methods, and constructor parameters, according to the set of applicable targets for the annotation. Type annotations on the types of record components are also propagated to the corresponding type uses in the automatically derived members.
  • Instances of record classes can be serialized and deserialized. However, the process cannot be customized by providing writeObject, readObject, readObjectNoData, writeExternal, or readExternal methods.
  • Record’s fields are final because the class is intended to serve as a simple “data carrier”.

4. Simple example of Record

Here is a simple example of Record:

record Rectangle(double length, double width) { }

public class TestRecord {
    public static void main(String[] args){
        Rectangle r = new Rectangle(4, 5);
        System.out.println(r);
        System.out.println("length: " + r.length());
        System.out.println("width: " + r.width());
    }
}

Output

Rectangle[length=4.0, width=5.0]
length: 4.0
width: 5.0

5. Record class and interfaces

A record class can implement interfaces. Here is an example:

record Rectangle(...) implements Drawable { }

6. Record Classes and Sealed Interfaces

You can name a record class in the permits clause of a sealed interface. Here is an example:

sealed interface Vehicle permits Bike {}

// Final subclass as a record
final record Bike(String brand, int gears) implements Vehicle {}

7. Constructors

Each class is provided with a default constructor. In case of a record, it is provided with a canonical constructor. that assigns all the private fields to the corresponding arguments of the new expression which instantiated the record. Here is an example:

record Point(int x, int y) {
    // Implicitly declared fields
    private final int x;
    private final int y;

    // Implicitly declared canonical constructor
    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

You can explicitly declare a constructor by matching the record header. You can also write the compact constructor by omitting the formal parameters. In the compact form, private fields corresponding to record components cannot be assigned in the body but are automatically assigned (this.x = x;) at the end of the constructor. Here is an example:

record SimpleInterest(int principal, int rate, int time) {
    SimpleInterest {
        this.principal = principal; //not allowed
    }
}

Here is another example where formal parameters are normalized:

record SimpleInterest(int principal, int rate, int time) {
    SimpleInterest {
        if(time < 0)
            throw new IllegalArgumentException("time can not be less than 0");
    }
}

Here is an example of explicitly declared constructor:

record SimpleInterest(int principal, int rate, int time) {
    SimpleInterest(int principal, int rate, int time) {
        this.principal = principal;
        this.rate = rate;
        this.time = time;
    }
}

8. Properties of record class

Consider a record class R declared as follows:

record R(T1 c1, ..., Tn cn){ }

If an instance r2 of R is created using an instance r1 of R in the following way:

R r2 = new R(r1.c1(), r1.c2(), ..., r1.cn());

Then r1.equals(r2) will always return true, assuming r1 is not null.

If you decide to declare your own accessor and equals method then you should confirm that this contract is not broken.

Here is an example of explicitly declared public accessor method:

record Rectangle(double length, double width) {
 
    // Public accessor method
    public double length() {
        System.out.println("Length is " + length);
        return length;
    }
}

You can declare static fields, static initializers, and static methods in a record class, and they behave as they would in a normal class, for example:

record Rectangle(double length, double width) {
    
    // Static field
    static double goldenRatio;

    // Static initializer
    static {
        goldenRatio = (1 + Math.sqrt(5)) / 2;
    }

    // Static method
    public static Rectangle createGoldenRectangle(double width) {
        return new Rectangle(width, width * goldenRatio);
    }
}

You cannot declare instance variables (non-static fields) or instance initializers in a record class. For example, following is not allowed:

record Rectangle(double length, double width) {
    int diagonal;  // Instance field is not allowed in record
}

9. Rules for record classes

Following are the restrictions on the declaration of a record class in comparison to a normal class:

  • A record class declaration does not include an extends clause. The superclass of a record class is always java.lang.Record, just as the superclass of an enum class is always java.lang.Enum. A normal class can extend java.lang.Object explicitly but a record class can not extend its implicit superclass Record.
  • A record class is implicitly final, and cannot be abstract.
  • The fields derived from the record components are final.
  • A record class is restricted from explicitly declaring instance fields and from containing instance initializers. These restrictions ensure that the state of a record value is defined solely by the record header.
  • The type an explicitly declared member must match the automatically derived member. Additionally, when implementing accessors or the equals and hashCode methods, it is crucial to maintain the semantic invariants of the record class.
  • A record class cannot declare native methods.

10. Local record classes

Sometimes your program can produce or consume values which are instances of record class. In this case, it is convenient to declare a local record inside a method. Here is such an example:

List<Employee> findTopSalesman(List<Employee> employees, int month) {
        // Local record
        record EmployeeSales(Employee employee, double sales) {}

        return employees.stream()
                .map(employee -> new EmployeeSales(employee, computeEmployees(employee, month)))
                .sorted((e1, e2) -> Double.compare(e2.sales(), e1.sales()))
                .map(EmployeeSales::employee)
                .collect(toList());
}

This means that their own methods cannot access any variables of the enclosing method. This is in contrast to local classes, where local classes are never static.

11. Local enum classes and local interfaces

Nested enum classes and nested interfaces are already implicitly static, so for consistency we define local enum classes and local interfaces, which are also implicitly static.

You can declare instance methods in a record class, independent of whether you implement your own accessor methods. You can also declare nested classes and interfaces in a record class, including nested record classes (which are implicitly static).

record Rectangle(double length, double width) {
    record SomeInnerRecord(){}
}

12. Static Members of Inner Classes

Before Java SE 16, it was not possible to declare an explicitly or implicitly static member in an inner class, except for constant variables. As a result, an inner class could not declare a record class member, since nested record classes are implicitly static.

Starting with Java SE 16, an inner class can declare members that are either explicitly or implicitly static, including record class members.

13. Annotations on record components

When a component in a record class is annotated, this annotation is applied to all of the elements to which this particular annotation is applicable. For example:

public final class MyClass {
    private final @CustomAnnotation T1 t1;
    private final @CustomAnnotation T2 t2;
    @CustomAnnotation T1 t1() { return this.t1; }
    @CustomAnnotation T2 t2() { return this.t2; }
}

The applicability of an annotation is declared using a @Target meta-annotation, for example @Target(ElementType.FIELD). If you want the annotation to be application to method declarations also then
@Target({ElementType.FIELD, ElementType.METHOD}) is used.

Following are the annotation propagation rules:

  • If an annotation on a record component is relevant to a field declaration, it will be applied to the corresponding private field.
  • If an annotation on a record component is applicable to a method declaration, it will be applied to the corresponding accessor method.
  • If an annotation on a record component is applicable to a formal parameter, it will be applied to the corresponding formal parameter of the canonical constructor if it is not explicitly declared, or to the corresponding formal parameter of the compact constructor if it is explicitly declared.
  • If an annotation on a record component is applicable to a type, the annotation will be propagated to all of the following:
    • the type of the corresponding field
    • the return type of the corresponding accessor method
    • the type of the corresponding formal parameter of the canonical constructor
    • the type of the record component (which is accessible at runtime via reflection)

14. Reflection API

Two public methods are added to java.lang.Class:

  • RecordComponent[] getRecordComponents(): Returns an array of java.lang.reflect.RecordComponent objects.
  • boolean isRecord(): Returns true if the given class was declared as a record.

15. Conclusion

In this tutorial, we’ve explored the concept of Record classes in Java, a powerful feature introduced in Java 14 and standardized in Java 16. Record classes offer a succinct and immutable way to model data, reducing boilerplate code and enhancing readability.