How to use Java Records
Tags: java, record, programming
Published: 2022-11-18 16:01:50

## Table of Contents
  * [Creating and using records](#creating-and-using-records)
  * [Documentation](#documentation)
  * [Default Values](#default-values)
  * [Validation](#validation)
  * [Normalization](#normalization)
  * [Modifying records](#modifying-records)
     * [Manually add wither methods](#option-1-manually-add-wither-methods)
     * [Use a compiler plugin](#option-2-use-a-compiler-plugin)
  * [Enforcing non-null](#enforcing-nonnull)
  * [Derived data](#derived-data)
  * [Data Transfer Objects](#data-transfer-objects)

## Creating and using records

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

```java
record Customer(UUID id, String name) {}

var customer = new Customer(UUID.randomUUID(), "John");
```

Retrieving the data is still done using getter methods, which have the same name as the <abbr title="The 'fields' of a records are called components">record components</abbr>.
Note that JavaBean conventions are not used, so a getter is called `x()` and not `getX()`:

```java
var name = customer.name();
```

## 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:

```java
/**
* Documentation of the Customer class.
* @param id customer id
* @param name customer name
*/
record Customer(UUID id, String name) {}
```

## 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.

```java
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();
```

## 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:

```java
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(), " ");
```

## Normalization

Besides validation, you can also modify data in a compact constructor.
This  is useful to [normalize](https://en.wikipedia.org/wiki/Canonical_form#Computing) your data:

```java
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();
```

## 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](https://mail.openjdk.org/pipermail/amber-spec-experts/2022-June/003461.html).

#### 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.

```java
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");
```

#### Option 2 -  Use a compiler plugin

The above is not particularly user-friendly.
Luckily compiler plugins can provide the missing feature, most notably [RecordBuilder](https://github.com/Randgalt/record-builder):

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

var customer = new Customer(UUID.randomUUID(), "John");
var renamed = customer.withName("John Doe");
```


## 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](https://github.com/uber/NullAway) or [Error Prone](https://errorprone.info/) to prevent `null` in your code in general, or you can add checks to your records:

```java
record Customer(UUID id, String name) {
    Customer {
        Objects.requireNonNull(id, "id cannot be null");
        Objects.requireNonNull(name, "name cannot be null");
    }
}
```

## 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:

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

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*:

```java
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)
}
```

An alternative is to use [validation](#validation) and add the field as a record component:

```java
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();
```

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();
```

## 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](https://dev.to/dhoepelman/records-and-jackson-4e96-temp-slug-8933937?preview=e082d91704b6c843212427eb87e0ab2360b8a0b5c6afda1886e4c4a3bd5c64274ba2f724c39c239c8b6b56681b66cf87389f93e5a143190dcdbeaa16).