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
, andhashCode
methodallows 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.