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
andhashCode
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
, orreadExternal
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 alwaysjava.lang.Record
, just as the superclass of anenum
class is alwaysjava.lang.Enum
. A normal class can extendjava.lang.Object
explicitly but a record class can not extend its implicit superclassRecord
. - A record class is implicitly
final
, and cannot beabstract
. - 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
andhashCode
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 ofjava.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.