An Introduction to Java Records for Beginners

Introduction
Java Records are a special kind of class introduced in Java 14, established as a standard feature in Java 16.
They are designed to encapsulate data, generating private final fields, an all-args constructor and the necessary methods for any regular class: public getters or accessor method, equals(), hashCode() and toString(). Setters are not generated, because the data are immutable.
Records extend the java.lang.Record
class, therefore they cannot extend any other class. And they can't be extended -they are final classes-.
record Person(String name, int age) { }
is equivalent to:
class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String name() { return name; }
public int age() { return age; }
@Override
public String toString() {
// Implementation
}
@Override
public boolean equals(Object o) {
// Implementation
}
@Override
public int hashCode() {
// Implementation
}
}
As we can see, records take out the verbosity of traditional Java classes.
Although IDEs can automatically generate these methods, the class has to be updated every time a new field is added.
Immutable Data
Record fields are immutable, which means that once created, they cannot be changed. Using the Person record declared above:
Person person1 = new Person("Jose", 30);
The following code will cause a compilation error:
person1.age = 22;
This immutability makes it easier to reason about the state of the objects and makes testing easier. However, this rule only applies to scalar fields. If there is a non-primitive or mutable field, its element can be modified:
record Person(String name, List<String> friends) { }
Elements of the friends
list can be modified.
Constructors
The all-arguments constructor is generated as shown above. It's also called the canonical constructor
.
The explicit constructor is called compact constructor
, and it allows developers to add custom logic during object initialization. A common use case is implementing validation rules.
record Person(String name, int age) {
// Unlike a class constructor, there is no a parameter list.
public Person {
Objects.requireNonNull(name);
Objects.requireNonNull(age);
if (age < 0) {
throw new IllegalArgumentException("Age must be a positive number.");
}
}
}
Records can have a default constructor that initializes all components to their default values. This must delegate to the canonical constructor:
record Person(String name, int age) {
public Person {
// call to the canonical constructor
this("Jose", 33);
}
}
// Usage
Person person = new Person(); // name = "Jose", age = 33
Custom constructors can also be created by mixing the canonical constructor and default constructors.
public record Person(String name, int age) {
public Person(int age) {
this("Jose", age);
}
}
// Usage
Person person = new Person("Philip", 52)
Person person = new Person(33); // name = "Jose", age = 33
This mimics the behaviour of languages thas has default values for their constructor parameters.
Accessing Records Components
The dot notation is used, as in any other class, but followed by the field name.
record Person(String name, int age) { }
Person person = new Person("Jose", 33);
String name = person.name();
int age = person.age();
The parentheses indicate that a getter method is being called.
Overriding Methods
The toString
, equals
and hashCode
methods can be overridden.
Implementing Methods inside Records
As with regular classes, static variables and methods can be defined inside records.
For example, something very close to a factory method can be achieved
record Person(String name, int age) {
public Person withName(String name) {
return new Person(name, age);
}
}
// Usage
Person person = person.withName("Jose");
Person otherPerson = person.withName("Philip");
or a validation method
record Person(String name, int age) {
public boolean isAdult() {
return age >= 18;
}
}
// Usage
Person person = new Person("Jose", 33);
boolean isAdult = person.isAdult(); // true
How to ensure that Records are immutable
Use defensive copies
record Person(String name, List<String> friends) { }
List<String> friends = new ArrayList<>();
Person person1 = new Person("Jose", friends);
friends.add("Philip");
friends.add("Steve");
Person person2 = new Person("Jose", List.copyOf(friends);
person.friends().add("Mafalda");
When to use Records
Use records for simple data carriers where immutability is required. For example, to represent configuration settings or for Data Transfer Objects (DTO) in RESTful services.
Disadvantages of Records
Records are final, which means they cannot be extended. This might be a limitation to use some Java frameworks.
The mixture of traditional classes and records is a bit confusing, since the getters do not use the same notation.
Being all the fields final might not be what we want in some cases. In languages like Kotlin, you can choose if a given field is going to be immutable:
// age is a mutable field and name is immutable
data class Person(val name: String, var age: Int)
Using Records with JPA
Entities are classes that are mapped to a database table. In popular JPA providers like Hibernate, entities are created and managed using proxies. Proxies are classes that extend the entity class, relying on the entity to have a no-args constructor and setters. Since records do not have setters and are final, they can't be used as entities.
However, starting with version 6.2, Hibernate supports record classes as embeddables.
@Embeddable
public record Person(String name, int age) { }
This annotation tells the persistence provider that this class can be embedded into entity objects.
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
@Embedded
private Person person;
private String email;
................
}
Take into account that the information represented by the record is immutable.
EntityManager entityManager = emFactory.createEntityManager();
User user = entityManager.find(User.class, userId);
// user.person.name and user.person.age are immutable
Conclusion
Java records make data class construction much easier than traditional POJOs, resulting in a much more readable code.
The syntax for records contructors is different from regular classes.
All the record member fields are immutable, but be careful when dealing with non-primitive data types whose elements might be modified.
Records could not be used as entities, but this has changed in recent JPA versions.
Bibliography
Subscribe to my newsletter
Read articles from José Ramón (JR) directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

José Ramón (JR)
José Ramón (JR)
Software Engineer for quite a few years. From C programmer to Java web programmer. Very interested in automated testing and functional programming.