- Lab
- Core Tech
![Labs Labs](/etc.clientlibs/ps/clientlibs/clientlib-site/resources/images/labs/lab-icon.png)
Guided: Java - Working with Records
In this hands-on lab, you will learn what Java records are: what their purpose is, the main concepts and the syntax. In a series of guided steps you'll learn what records are and how to write code that uses them. After completing this lab you'll be ready to use records in your own Java projects.
![Labs Labs](/etc.clientlibs/ps/clientlibs/clientlib-site/resources/images/labs/lab-icon.png)
Path Info
Table of Contents
-
Challenge
Data Transfer Objects
Data Transfer Objects
A common design pattern is the Data Transfer Object (DTO). The most straightforward way to define a DTO in Java is by creating a class with fields and corresponding getter and setter methods, and optionally
equals()
,hashCode()
andtoString()
methods.Let's start by defining a simple DTO in this way.
-
Challenge
Immutability
Immutability
It is often a good idea to make Data Transfer Objects immutable. This means that you define the class in such a way that the content of the object cannot be modified after it has been created.
This has a number of advantages, for example:
- It makes programs easier to understand and helps you to avoid bugs.
- Immutable objects can safely be cached and reused.
- Immutable objects are thread-safe; they can be used across multiple threads without synchronization.
- Hash-based collections such as
HashMap
expect that the hash code of objects used as keys does not change, which is automatically the case for immutable objects.
The JDK contains a number of immutable classes. For example, class
String
, the wrapper classes for primitive types (Integer
,Long
etc.) and the classes in the packagejava.time
are all immutable classes.There are a number of steps to make a Java class immutable:
- Make the fields of the class
final
, so that they cannot be modified after the object is initialized in the constructor. - Provide a constructor that takes an argument for each field and that initializes the fields using these arguments.
- Do not write setter methods.
- Make the class itself
final
, so that it's not possible to make a mutable subclass.
Let's change class
Customer
to make it immutable. -
Challenge
Records
Records
As you have experienced in the previous steps, creating an immutable DTO with a regular class in Java is quite verbose and there are a number of things you need to remember to make sure that your class is really immutable. Records are a way to make this easier, with a more concise syntax and rules to ensure immutability.
To define a record, use the keyword
record
instead ofclass
. Instead of declaring fields, a constructor and getter methods, list the fields directly after the name of the record, between parentheses. Here is an example:import java.math.BigDecimal; import java.util.Currency; public record Amount(Currency currency, BigDecimal value) { }
This is like defining a regular class with two
final
fields, a constructor that takes two arguments to initialize the fields and two getter methods for the fields.Let's define the
Customer
DTO as a record instead of as a class. -
Challenge
Instantiating Records and Accessing Members
Instantiating Records and Accessing Members
Records are really just normal Java classes, defined with an alternative syntax. The compiler will generate a regular Java class for a record and it will automatically generate the following:
- A
private final
field for each of the components of the record. - A constructor that takes an argument for each of the components and that initializes all the components with the argument values by assignment.
- An accessor method for each component (similar to a getter method; see below).
equals()
andhashCode()
methods based on all the components of the record.- A
toString()
method.
Because a record is really just a class, creating an instance of a record works exactly the same a creating an instance of a class, by using
new
. Here is an example:var amount = new Amount(Currency.getInstance("USD", new BigDecimal("89.95")));
To access the components of a record, call the appropriate accessor method, which is a method that has the same name as its corresponding component. Note that the names of accessor methods do not start with
get
. For example:var currency = amount.currency(); // Note: currency(), not getCurrency() var value = amount.value(); // Note: value(), not getValue()
Let's practice with this using our
Customer
record. - A
-
Challenge
The Canonical Constructor
The Canonical Constructor
The constructor that the compiler automatically generates for a record is called the canonical constructor. The parameter list of the canonical constructor matches the list of components of the record - it takes an argument for each component. What the automatically generated canonical constructor does is initialize each field by assigning the corresponding argument to it.
You can choose to define the canonical constructor yourself. When you do this, the compiler will not automatically generate a constructor; the one you've defined yourself will be used instead.
There are two main reasons why you might want to define the canonical constructor yourself:
- To validate argument values.
- To make a defensive copies of mutable argument objects. (We'll come back to this later).
The syntax for defining your own canonical constructor is the same as in a regular class. Here is an example:
import java.math.BigDecimal; import java.util.Currency; public record Amount(Currency currency, BigDecimal value) { public Amount(Currency currency, BigDecimal value) { // Validate argument values if (currency == null) throw new IllegalArgumentException("currency must not be null"); if (value == null) throw new IllegalArgumentException("value must not be null"); // Initialize fields this.currency = currency; this.value = value; } }
Let's write our own canonical constructor for record
Customer
to validate values. -
Challenge
The Compact Constructor
The Compact Constructor
Take a look again at how you define a canonical constructor yourself in a record.
import java.math.BigDecimal; import java.util.Currency; public record Amount(Currency currency, BigDecimal value) { public Amount(Currency currency, BigDecimal value) { // Validate argument values if (currency == null) throw new IllegalArgumentException("currency must not be null"); if (value == null) throw new IllegalArgumentException("value must not be null"); // Initialize fields this.currency = currency; this.value = value; } }
This syntax is more verbose than it needs to be. There are two issues:
- We are repeating the list of components as the parameter list of the constructor.
- We are explicitly assigning the argument values to the fields.
The point of records is to provide you with a concise syntax to define immutable DTOs, yet when we define a constructor, it quickly becomes verbose again.
To stay with the spirit of concise syntax, Java provides an alternative syntax to define the canonical constructor of a record, which is the compact constructor syntax. It looks like this:
import java.math.BigDecimal; import java.util.Currency; public record Amount(Currency currency, BigDecimal value) { public Amount { // Validate argument values if (currency == null) throw new IllegalArgumentException("currency must not be null"); if (value == null) throw new IllegalArgumentException("value must not be null"); } }
This tackles exactly the two points mentioned above by doing the following:
- Specify the constructor without a parameter list.
- Don't initialize the fields in the constructor. The compiler will automatically generate code to do this.
Of course, in a compact constructor, you can still validate argument values.
Let's apply this to record
Customer
. -
Challenge
Additional Constructors
Additional Constructors
Just like a regular class can have multiple constructors with different parameter lists, a record can have more than one constructor. However, there is a special rule that you will have to keep in mind when defining an additional constructor for a record.
The rule is this: Any constructor other than the canonical constructor must have a
this(...)
statement to call one of the other constructors.The consequence of this rule is that when you instantiate a record, ultimately the canonical constructor will always be called first, which initializes the fields of the record.
Let's add an additional constructor to record
Customer
. -
Challenge
Automatically Generated Methods
Automatically Generated Methods
The compiler automatically generates
toString()
,equals()
andhashCode()
methods for records.The automatically generated
toString()
method returns a string that contains the values of the components of the record, mainly useful for logging and debugging.The
equals()
method compares two instances of the record by callingequals()
on all the components (considering the record instances equal if all components are equal), and thehashCode()
method computes a hash code based on all the components.It's possible to implement these methods manually. If you do that, your own implementation will replace the version that the compiler would generate automatically.
Let's call the
toString()
method onCustomer
to see what the automatically generatedtoString()
method returns. -
Challenge
Shallow Immutability
Shallow Immutability
One thing that is important to keep in mind when working with records is that they are only shallowly immutable.
When you use a mutable object as a component for your record, then your record is not really immutable. You will need to take extra steps to make sure that instances of your record cannot be modified after being created.
A common use case where this is important is if your record has a component that is for example a
java.util.List
. Java's standard collection classes are mutable.To make sure that instances of your record are unmodifiable, you must make a defensive copy of the argument in the constructor of the record.
Here is an example:
import java.util.List; public record KeyValues(String key, List<String> values) { public KeyValues { // Make a defensive, unmodifiable copy of the argument list values = List.copyOf(values); } }
Here, we have implemented the canonical constructor of record
KeyValues
using the compact constructor syntax.Inside the constructor, we make a defensive copy of the list that is passed as an argument. Note that
List.copyOf(...)
creates an unmodifiable copy of the list.Also, we are re-assigning the return value of
List.copyOf(...)
back to the argument variable itself. Remember that when you use the compact constructor syntax, the compiler will automatically generate assignment statements to initialize the components of the record with the argument values.Let's continue with record
Customer
to see this in action. -
Challenge
Implementing Interfaces
Implementing Interfaces
A record cannot extend a superclass, but it can implement interfaces. The way this works is the same as for regular classes.
Let's try this with record
Customer
. -
Challenge
Builders for Records
Builders for Records
A common design pattern when you work with immutable DTOs is the builder pattern.
You create a special class, the builder, which contains methods to gather the values for the fields which can be used to create an immutable object using these values.
Advantages of the builder pattern are:
- When you call the methods of builder to supply values, it's very clear which value means what. Compare this to creating an object by calling a constructor; only the position of the values in the argument list tells you which value is which.
- It is more flexible. You don't need to supply all values to the builder; the builder can decide to use a default if you don't supply a value. You don't need to implement multiple constructors to account for all possible combinations.
A disadvantage is that requires you to write extra code for the builder itself.
Take a look at record
Customer
for this step. Included in it is aBuilder
as a static nested class. -
Challenge
Summary
Summary
Congratulations! You've learned what records are in Java and how to work with them. We've covered the following topics:
- The most common use case for records is to define immutable Data Transfer Objects.
- A record is just like a class, but defined with a different syntax.
- The compiler automatically generates an accessor method for each record component, that has the same name as the
component (it is not named
get...()
). - The automatically generated canonical constructor initializes all the components of the record.
- You can choose to implement the canonical constructor yourself to validate arguments and make defensive copies of mutable arguments.
- You can use the compact constructor syntax to keep your code mode concise.
- You can define additional constructors, but any additional constructor must call another constructor with a
this(...)
call. - Records are only shallowly immutable. You must make defensive, unmodifiable copies of mutable objects.
- Records can implement interfaces, just like regular classes.
- The builder pattern is a useful design pattern when working with records.
What's a lab?
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Provided environment for hands-on practice
We will provide the credentials and environment necessary for you to practice right within your browser.
Guided walkthrough
Follow along with the author’s guided walkthrough and build something new in your provided environment!
Did you know?
On average, you retain 75% more of your learning if you get time for practice.