In Part II of the "What's new in Java 15" series, we will explore what a Java Record is, compare it to existing language features and other languages, and look at how it can be used in a Spring Boot application to simplify the code.

What is a Java Record

A Java Record is a new way to declare a type in the Java language. Records were introduced to the Java language to reduce the boilerplate associated with Plain Old Java Objects (POJO). When creating a good POJO, a developer must implement an equals method, a toString method, and the corresponding getters. From POJO to POJO, the implementations are exactly the same, the only thing that changes is the name of the properties for the type. Although IDEs and projects like Lombok have created features which auto generate this boilerplate code, having all of this boilerplate can get in the way of understanding what the POJO represents.

A Record does the following:

  • generates one public constructor with all of the properties

  • marks all properties as private final

  • creates public getter methods for all properties

  • creates a toString , equals , and hashCode method

  • allows properties to be decorated with annotations

A Record cannot do the following:

  • declare any other instance variables
  • extend any other class

Java's Record type is very similar to Kotlin's Data Classes. One noticeable difference in the API generated by Java and compared to the one generated by Kotlin is Java's Record lacks a copy function.

To read more about a Record , checkout Java's documentation on it.

The Code

Alright, enough talk, let's take a look at some code.

The following code snippet is a traditional implementation of a POJO using classes. For this example, we will be modeling a person. A person has two attributes, name and occupation.

import java.util.Objects;

public class Person {
    private final String name;
    private final String occupation;

    public Person(String name, String occupation) {
        this.name = name;
        this.occupation = occupation;
    }

    public String getName() {
        return name;
    }

    public String getOccupation() {
        return occupation;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return Objects.equals(name, person.name) &&
                Objects.equals(occupation, person.occupation);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, occupation);
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", occupation='" + occupation + '\'' +
                '}';
    }
}

As is evident from the above code, creating a good POJO with getters, equals , hashCode , toString and a constructor is very verbose. To implement a POJO with two properties was 47 lines of code. POJOs in the wild will almost certainly have more than two properties. This POJO also has a huge maintenance cost. Each time a new property is added, the constructor, getters, equals , hashCode , and toString methods all have to be modified.

Lombok attempted to solve this problem with the @Value annotation. The same person class written with Lombok:

import lombok.Value;

@Value
public class Person {
    String name;
    String occupation;
}

This code is much simpler. It reduces the boilerplate by hiding it behind the @Value annotation and it eliminates any maintenance cost associated with this POJO. However, the @Value annotation is not a native language feature and requires a new dependency to be added to the project. Lombok can also be confusing to new developers on a codebase.

Java Records allow for the Person class to be modeled in a succinct way and are included in the native language.

To declare a record, give it a name and declare the properties on the record. That's it. Java will take care of the rest. No boilerplate. No sifting through 50 lines of code to figure out what data the class models.

public record Person(String name, String occupation) {}

Records can be used in the following ways:

Person p1 = new Person("test1", "software engineer");
Person p2 = new Person("test2", "test engineer");
Person p3 = new Person("test1", "software engineer");

System.out.println(p1.toString()); // Person[name=test1, occupation=software engineer]
System.out.println(p1.name()); // "test1"
System.out.println(p1.occupation()); // "software engineer" 
System.out.println(p1.equals(p2)); // false
System.out.println(p1.equals(p3)); // true

Real Life Examples

POST Request Body

We have now learned about what a Record is and how to use it. But how can we actually use a Record in the wild? When creating a REST endpoint with Spring Boot and Spring Web, POST Request Bodies are often declared as a POJO. A Record class can be used to simplify the code needed to model the request body. The following example declares a REST controller and a POST endpoint which accepts the Person model from earlier in the request body.

// PersonController.java
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/person")
public class PersonController {

    @PostMapping
    public void post(@RequestBody PersonRequest person) {
        System.out.println(person);
    }
}

// PersonRequest.java
public record PersonRequest(String name, String occupation) {}

Starting up the application and making the following curl request results in a response code of 200 OK and a log message with the person request on the console.

curl --location --request POST 'localhost:8080/person' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "test1",
    "occupation": "Software Engineer"
}'

Since records can be decorated with annotations, we can make use of the Spring Validation Framework annotations.

// PersonController.java
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/person")
public class PersonController {

    @PostMapping
    public void post(@Validated @RequestBody PersonRequest personRequest) {
        System.out.println(personRequest);
    }
}

// PersonRequest.java
import javax.validation.constraints.NotNull;

public record PersonRequest(@NotNull String name, @NotNull  String occupation) {}

Now, when making a request with a missing name, there will be a status code of 400 Bad Request returned since we have required name to be on the request body with the @NotNull annotation.

curl --location --request POST 'localhost:8080/person' \
--header 'Content-Type: application/json' \
--data-raw '{
    "occupation": "Software Engineer"
}'

Conclusion

In this post we learned about what a Java Record is, learned how to use it, and went through an example of how to use a Record in the real world. An example Spring Boot application can be found with test cases on my GitHub profile.

This post is also available on DEV.