Have you been wondering about Java records? With the release of Java 14, we can now use records to easily create immutable data objects.
Very often we have to pass immutable data between objects, components and/or systems. This data could be a result of a database query, or some information from a web service. We refer to this type of data as data transfer objects (DTOs). Domain objects persisted using an ORM framework also fit into this category.
We then need a class that holds this data. The data must be immutable, so we don’t have to worry about synchronization.
This forces us to create a class with the necessary final fields and a number of standard boilerplate methods. Defining a class for every domain object that we want to pass around obviously becomes tedious, repetitive and error-prone.
Our data classes would need the following:
- A
private final
field for each data member in the class. - A getter method for each field. No setters would be needed.
- A public constructor with an argument for each data field.
- The three usual canonical methods:
equals()
,hashCode()
andtoString()
.
You might find it useful to revisit the previous posts on JavaBeans and Canonical classes for revision.
What is a Java Record?
A record is a class that is intended to hold pure immutable data. By declaring a type as a record, we are clearly expressing our intention that this type represents only data that cannot be altered.
Records were not designed only to reduce boilerplate code. They are also a way to easily and concisely create immutable data classes. We should only use them where we need that behaviour. Records are not meant to generate mutable classes using the JavaBean naming conventions.
Implementation of Java Records
The syntax for declaring a record is simpler and more concise than a normal Java class. It needs only the type and name of the required fields. It is similar to an enum
in that it is a restricted class with special semantics.
Here is a simple example of a record modelling a Person
domain object/DTO:
record Person(String name, int age, boolean gender) {}
That’s it! Wow!
The private final instance fields and a public constructor with an argument for each field are generated automatically by the Java compiler. The compiler also generates the equals()
, hashCode()
, and toString()
methods.
To see what code is generated, we can use the JDK javap
disassembler. The default output of javap
is the following:
public final class Person extends java.lang.Record { public Person(java.lang.String, int, boolean); public final java.lang.String toString(); public final int hashCode(); public final boolean equals(java.lang.Object); public java.lang.String name(); public int age(); public boolean gender(); }
The constructor and canonical methods have standard method signatures. The accessor methods don’t follow the JavaBeans naming convention, but rather use the field name as the accessor method name.
The following simple code snippet tests the Person
record:
Person p = new Person("Fred", 34, true); Person q = new Person("Mary", 43, false); System.out.println("Name: " + p.name()); System.out.println("Age: " + p.age()); System.out.println("Gender: " + p.gender()); System.out.println("hashCode: " + p.hashCode()); System.out.println("toString: " + p.toString()); System.out.println("p equal to q? " + p.equals(q));
Records were a preview feature in Java 14, so when we compile a record
using Java 14, we need to use the --enable-preview -release 14
options. If we’re using a later version of Java, then we can compile as normal without that option.
Record as a Restricted Identifier
The word record
is a restricted identifier similar to var
. It isn’t a keyword (yet), which means the following code is valid:
int record = 10; void record() {}
However, it would probably be a bad idea to use it as an identifier because some future version of Java might promote it to a keyword.
What’s Next?
We’ve just scratched the surface of records. They have a number of additional features, such as allowing us to overload the automatically generated constructors and methods. We can also use generic types in the arguments of records, and we can create static fields and methods.
We’ll look at these features next week. Until then, keep learning!
As always, please share your comments and questions.