Use Records to Simplify Your Java Code
1. Introduction
Records are a special type of Java class, whose first goal is to reduce the ceremony required to write some Java code. They were introduced by the JDK Enhancement Proposal(JEP) 395 as part of the JDK 16. In this article, you will learn more about Records and how to effectively use them in your Code.
2. Prerequisites
Records are available from Java 16 onwards. In this tutorial, we will use Java 17.
Hence, to complete the tutorial, you will need :
- The Java SE Development Kit 17 (JDK 17): If you don't have it already refer to this tutorial for the installation procedure.
3. Motivation and Goals
Imagine you are building an e-commerce API. Suppose you have an endpoint to add new products to your catalog. Normally, your endpoint would take a product DTO (Request) and return another DTO(response). For simplicity, we assume that you have a ProductRequest class with the following fields: name(String), price(BigDecimal).
Without Records, you will have to create the following Java class:
class ProductRequest{
private String name;
private BigDecimal price;
public ProductRequest(String name, BigDecimal price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public BigDecimal getPrice() {
return price;
}
}
This class has almost 15 lines of code. However, its only purpose is to carry the data from the user to the Controller endpoint. Such a class is also called a data carrier. The problem here is that the class will only be used to access its field values(no mutability). There won't be any particular logic implementation in the class. So many lines of code for such a simple need. That's why Records were introduced.
Let's now see how you can achieve the same thing with Java records:
record ProductRequest(String name, BigDecimal price){}
As you can see, the code is a one-liner. The developer can hence focus on more important parts of the application.
4. Definition
A record is a special kind of Java class designed to model immutable objects and simplify the creation of data-carrying classes without having to write boilerplate code such as constructors, getters, toString()
, equals()
, and hashCode()
.
The basic syntax to create a record class is the following:
access_modifier record ClassName(list of components) {}
Below is an example of record
and its usage:
//Record declaration
public record Person(String firstName, String lastName){}
//Usage as a normal class
public static void main(String[] args){
Person person = new Person("John","Doe");
System.out.println(person.firstName());//John
}
When you declare a record, the Java compiler will automatically generate the following:
- A private final field for each item in the list of components of the record declaration.
- A public zero-argument get method(also called getter) with the same name as the field.
- A public constructor with a list of fields that matches the list of components in the record declaration: name, type, and order. Such a constructor is called the canonical constructor of the record.
- The compiler also generates an implementation for the following methods:
toString()
,equals()
, andhashCode()
.
5. Constructors of a Record Class
5.1. Canonical constructor
As we saw earlier, Java generates a constructor with parameters that match the list of components of the record. This constructor is known as the normal canonical constructor. If for some reason, you need to customize it, you may provide a constructor whose signature matches the one of the canonical constructor.
record Person(String firstName, String lastName){
public Person(String firstName, String lastName){
Objects.requireNonNull(firstName);
Objects.requireNonNull(lastName);
this.firstName = firstName;
this.lastName = lastName;
}
}
With this custom constructor, we ensure the record fields are not null. However, as you can notice the component list is repeated in the record header and the constructor. To overcome this, there is a shorter form of the canonical constructor:
record Person(String firstName, String lastName){
public Person{
Objects.requireNonNull(firstName);
Objects.requireNonNull(lastName);
}
}
This form is called the compact canonical constructor and eliminates the need to repeat the component list.
5.2. Non-Canonical constructors
Apart from the canonical constructor, you can provide any number of non-canonical constructors. These are constructors whose signatures don't match that of the canonical constructor. The zero-argument constructor is an example of a non-canonical constructor.
//non-canonical constructor, no-argument constructor
public Person(){
this("John","Doe");
}
Important: While creating a non-canonical constructor, you MUST use the
this
operator to chain your constructors and delegate eventually to the canonical constructor.
6. Features of the Record API
6.1. Explicit declaration of Record Class members
By default, the record will provide a getter for every field in the component list as well as implementations of toString()
, equals()
, and hashCode()
methods. However, you are free to provide a custom implementation for any of these methods.
@Override
public String firstName() {
System.out.println(firstName);//custom behaviour
return firstName;
}
Additionally, you may add any number of new methods:
public String fullName() {
return firstName + " "+lastName;
}
6.2. Implementing Interfaces
A record class can implement an interface.
record Person(String firstName, String lastName) implements Serializable {}
ProTip: Since a record cannot inherit behaviour from a superclass via inheritance, using interfaces and
default
methods is a great workaround.
7. Restrictions of the Record API
Some restrictions apply to the record API.
7.1. No Inheritance
A record cannot extend another class, should that class be a record or not. The following code will not compile:
record Student(String firstName, String lastName) extends Person {}//doesn't compile
Info: The
extends
clause is not allowed in the record header.
7.2. No instance fields
Apart from the fields defined as part of the record components, no other fields can be defined in the record body. Any other declared field must be static
.
record Student(String firstName, String lastName){
private String fullName;//Doesn't compile
}
7.3. Record cannot be abstract
You cannot declare a record class to be abstract
because they are by definition final
. Hence, no class can inherit from a Record class.
The following code will not compile:
abstract record Student(String firstName, String lastName){//abstract keyword not allowed
}
Beyond these restrictions, records behave like any normal Java class.
8. Use Cases
Some of the main use cases for Records are(but not limited to):
- Data Transfer Objects(DTO)
- API Responses
- Configuration classes
- Immutable Data Structures
8.1. Data Transfer Objects(DTO)
A Common use case for records is DTOs, which are used to carry data in your application from one layer to another. As an example, you can use a DTO to carry the data from the business layer (Service) to the database layer(DAO), and vice-versa.
record ProductDTO(String name, BigDecimal price){}
8.2. API Responses
You may also use a record to hold the requests and responses to your API Calls. Here is an example with an API response.
record ProductResponse(String name, BigDecimal price, HttpStatus status){}
8.3. Configuration classes
Another use case for a record is for holding the settings for your application. Here is a simplistic example:
record AppConfig(String appName, String appVersion, String env){}
8.4. Immutable Data Structures
By definition, records are immutable. So they are a perfect match whenever you want to achieve immutability.
record Product(String name, BigDecimal price){}
The Product class
provides getters for its fields, but NO setters. Hence, the class is immutable.
9. Conclusion
In this tutorial, you learned how to use Records in your Java applications. This post was a quick overview of the record feature, visit the official Javadoc for more insights.
_This article was originally published at: https://nkamphoa.com