DEV Community

Cover image for How to use Java Records
David Hoepelman
David Hoepelman

Posted on • Originally published at xebia.com

How to use Java Records

Table of Contents

Creating and using records

Using a record is almost exactly like using an immutable POJO.
Creating one is done using the constructor:

record Customer(UUID id, String name) {}

var customer = new Customer(UUID.randomUUID(), "John");
Enter fullscreen mode Exit fullscreen mode

Retrieving the data is still done using getter methods, which have the same name as the record components.
Note that JavaBean conventions are not used, so a getter is called x() and not getX():

var name = customer.name();
Enter fullscreen mode Exit fullscreen mode

Documentation

Documentation for records can be added like you would add it to a class. A component can be documented using the existing @param javadoc tag:

/**
* Documentation of the Customer class.
* @param id customer id
* @param name customer name
*/
record Customer(UUID id, String name) {}
Enter fullscreen mode Exit fullscreen mode

Default values

Records can have additional constructors, next to their canonical constructor that has the same parameters as the components.
This can be useful to set default values for some components.

record Customer(UUID id, String name) {
    /** Create a new customer with a fresh id. */
    public Customer(String name) {
        this(UUID.randomUUID(), name);
    }
}

var customer = new Customer("John");
UUID generated = customer.id();
Enter fullscreen mode Exit fullscreen mode

Validation

Often, you do not want to allow all values in a record component, but want to restrict them to what makes sense in the context of what your record represents.

Records provide a special construct called a compact constructor to facilitate this.
They work similar to a normal constructors, but you don't need to specify parameters or set the components:

record Customer(UUID id, String name) {
    public Customer {
        if(name.isBlank()) {
          throw new IllegalArgumentException("name cannot be empty.");
        }
    }
}

// This will throw an IllegalArgumentException 
var invalidCustomer = new Customer(UUID.randomUUID(), " ");
Enter fullscreen mode Exit fullscreen mode

Normalization

Besides validation, you can also modify data in a compact constructor.
This is useful to normalize your data:

record Customer(UUID id, String name) {
    Customer {
        name = name.trim();
    }
}

Customer customer = new Customer(UUID.randomUUID(), "John \n");
// This will be "John"
String name = customer.name();
Enter fullscreen mode Exit fullscreen mode

Modifying records

Unfortunately records are not as easy to modify as their equivalents in other languages.
There are currently two viable options to modify records. A future version of Java might support the use case better.

Option 1 - Manually add wither methods

In plain Java, you can manually specify a method that returns a modified copy.
The most common naming convention for this is withX, hence the name wither methods.

record Customer(UUID id, String name) {
    Customer withName(String name) {
        return new Customer(id, name);
    }
}

var customer = new Customer(UUID.randomUUID(), "John");
var renamed = customer.withName("John Doe");
Enter fullscreen mode Exit fullscreen mode

Option 2 - Use a compiler plugin

The above is not particularly user-friendly.
Luckily compiler plugins can provide the missing feature, most notably RecordBuilder:

@RecordBuilder
record Customer(UUID id, String name) {}

var customer = new Customer(UUID.randomUUID(), "John");
var renamed = customer.withName("John Doe");
Enter fullscreen mode Exit fullscreen mode

Enforcing non-null

A special kind of validation is enforcing that record fields are not null. (Un)fortunately, records do not have any special behavior regarding nullability.
You can use tools like NullAway or Error Prone to prevent null in your code in general, or you can add checks to your records:

record Customer(UUID id, String name) {
    Customer {
        Objects.requireNonNull(id, "id cannot be null");
        Objects.requireNonNull(name, "name cannot be null");
    }
}
Enter fullscreen mode Exit fullscreen mode

Derived data

Sometimes, you need to use the primary pieces of data in a record to derive another piece of data. Just like with POJOs, you can simply add a method:

record Customer(String firstName, String lastName) {
    public String fullName() {
        return String.format("%s %s", firstName, lastName);
    }
}
Enter fullscreen mode Exit fullscreen mode

Just like with POJOs, these values are lazily calculated and not cached.
Unlike with POJOs, you cannot make this eager and cached by adding a field, because records are not allowed to have fields:

record Customer(String firstName, String lastName) {
    // This will give a compile error, records are not allowed to have fields
    private final String fullName = String.format("%s %s", firstName, lastName)
}
Enter fullscreen mode Exit fullscreen mode

An alternative is to use validation and add the field as a record component:

record Customer(String firstName, String lastName, String fullName) {
    public Customer {
      fullName = String.format("%s %s", firstName, lastName);
    }

    public Customer(String firstName, String lastName) {
        this(firstName, lastName, null);
    }
}

var customer = new Customer("John", "Doe");
// This will be "John Doe", and is already computed and cached
var fullName = customer1.fullName();
Enter fullscreen mode Exit fullscreen mode

However, be careful with doing this, as it might be surprising to users of the record. In general, I would recommend using a POJO with a private final field instead.

var customer = new Customer("John", "Doe", "Austin Powers");
// This will unexpectedly be "John Doe" instead of "Austin Powers"
var fullName = customer.fullName();
Enter fullscreen mode Exit fullscreen mode

Data Transfer Objects

Another important use case for records is to transfer data with a REST API or to and from the database.
For JSON this is often done with the Jackson library.
Continue on to part 3: Records and Jackson.

Top comments (0)