Exploring Java 14 Records: Simplifying Data Classes in Java
0 CommentsLast Updated on October 21, 2024 by jt
Introduction
Records are a new feature in Java 14. We can use them to avoid a lot of boilerplate code in standard DTO classes, save our time, and limit space for errors.
In this tutorial, we will show what records are and how we can use them in our code to make it more readable and concise. We will also tell something about their design, when we can use them and what restrictions they have.
Setup
For this tutorial, we will use the IntelliJ IDEA IDE, version 2020.1.
We will also use Java 14, Gradle in version 6.3, and Spring Boot 2.2.6.
For Java 14 records to work with IntelliJ IDEA and Gradle, --enable-preview
flag has to be used. Because of that, we will have to do some simple configuration.
In IntelliJ IDEA, go to File -> Project Structure -> Project
and set Project SDK
to Java 14
(if you do not have this version, download JDK 14 manually or through IDEA tool) and Project Language Level
to 14 (Preview) - Records, patterns, text blocks
.
As for Gradle, In build.gradle
file:
sourceCompatibility = 14 tasks.withType(JavaCompile) { options.compilerArgs += '--enable-preview' } tasks.withType(Test) { jvmArgs += "--enable-preview" }
What is record
A record in Java 14 is a new kind of type declaration. It is similar to enum in that it is a restricted form of class and it allows us to create a specific kind of object types more easily.
A lot of users that used Java complained about having to write many getters, setters, and a few overridden methods, such as equals()
, hashCode()
, or toString()
for objects that are simple data containers. Along with other language structures that commonly occur in such classes we can see that there is a lot of repetitive and error-prone (equals()
and hashCode()
implementations, for example) code, that does not provide much value.
We should keep in mind that records were not designed to be just boilerplate reductors. Above all, their intention is to provide programmers with a way of creating clear, concise, and immutable data aggregate classes. Therefore, we should use them in places where we need such constructs.
Record example
public record Product(String name, double price) {}
Above we have an example of Product
record. Product
is the name of the record and it also has a state description, which describes the components of the record. In our case, those components are called name
and price
. The body in record is optional, so if we do not need it, we can leave it empty.
As the record aims to be a simple representation of data, it does create some things under the hood for us. To summarize:
- a private and final field for each of the components
- a public accessor method for each of the components – where name and type are the same as for the component
- a public constructor – its signature is the same as the state description of the record. It initializes each of the fields with a corresponding argument
- equals() and hashCode() methods – two records will be equal if they have the same type and state
- toString() method – it includes a string representation of all the components along with their names
As we can see, all of the code we would have to write by ourselves or use some other tool to do this for us now is provided by the Java language itself.
Let us try to see what we can do with a record:
Product product1 = new Product("bread", 1.50); System.out.println(product1.name()); System.out.println(product1.price()); System.out.println(product1.toString()); Product product2 = new Product("apple", 1.50); System.out.println(product1 == product2); System.out.println(product1.equals(product2));
bread 1.5 Product[name=bread, price=1.5] false false
As we can see, records behave just like normal classes. In place of getters, we have methods named after record components. Records are immutable, so we do not have setters at all.
Explicit declaration of record members
There is a possibility to declare any of the automatically generated methods by hand. Keep in mind that this should be done with care, as it is easy to break the underlying semantic invariants of the records.
Let us see an example of such a declaration:
public record ExplicitMemberProduct(String name, double price) { public String name() { return "Product_" + name; } }
Record declaration is the same as in the previous example. Although in this one we have explicitly declared an accessor for a name in the record body. It is a standard accessor, as many others in a normal class declaration. The only difference is that we do not have a commonly used get
prefix.
We can try to call this getter, to see what result we will get:
ExplicitMemberProduct explicitMemberProduct = new ExplicitMemberProduct("milk", 2.50); System.out.println(explicitMemberProduct.name());
Result:
Product_milk
Explicit declaration of record members – constructor
Explicit constructor declaration in records requires a separate explanation, the one that signature matches the state description of the record. We can declare such a constructor without a formal parameter list and as a result, it will have the same parameters as the record state description.
The most important part here though is that each field that was definitely unassigned when leaving the constructors body, will have a value implicitly initialized from corresponding formal parameter (in other words, if we don’t initialize, let us say, name
in the constructor body, it will be initialized like this.name = name
).
Let us see that on the example:
public record ExplicitConstructorProduct(String name, double price) { public ExplicitConstructorProduct { price = 5.0; } }
Above we have a record that uses an explicitly declared constructor. In there, we set price
to 5.0
and leave name
for implicit initialization. Result of instance creation and calling of toString() method on this object you can see below:
ExplicitConstructorProduct explicitConstructorProduct = new ExplicitConstructorProduct("soap", 3.00); System.out.println(explicitConstructorProduct.toString());
ExplicitConstructorProduct[name=soap, price=5.0]
As a result name
has a value that was implicitly initialized with a formal parameter.
Records restrictions
Aside from the things we can do with records, there are also some things we cannot do. These things are:
- records cannot extend any other classes
- they cannot declare fields other than private final ones that correspond to components of the state description, however, we can define static variables
- we cannot make them abstract
- record is implicitly final
- to keep immutability, record components are final
- nested record is implicitly static
In cases other than the ones mentioned above, records behave like normal classes.
Summary
Java makes the progress to be more programmer-friendly with each new version of the language. The new feature, records, is a great way to create plain data types in Java.
Records are immutable and concise, which makes them very easy to understand in the code that uses them. Thanks to them, we now have a language construct, that we can use instead of IDE autogenerated code or additional dependencies, such as Lombok.
In this tutorial, we have shown the basic usage of records. If you are interested in knowing more about that, be sure to check the JDK Enhancement Proposal here.