Java Records
Sometimes you will come across classes in your projects that are specifically meant for storing data. For these, the layout is always the same: you have your variables, getters, and a constructor that takes all the information to create the object, and, in most cases, you will also want to override the toString()
, equals()
, and hashCode()
methods. This is where Records come to your rescue.
Records
A record, like classes, is a way to declare a new type in Java. Now you may wonder why we need records when we already have classes to do the same job. The answer is boilerplate code or, more precisely, boilerplate reduction. The biggest difference from something like Lombok is that Records have language-level support, which means you don't need any third-party dependencies, so they are more attractive to use. They basically are value objects out of the box as they are immutable: their state cannot be modified once created.
Defining records
A new Java Record is declared with the record
keyword followed by the name of the record and ending with parentheses. Let's create an empty record named User
for now. It would look like this:
record User() {
// empty body
}
Ok, now we have created our first record, but it's not exactly useful just yet. Let's give User
a username and a password. We'll do that by writing the variables inside the parentheses:
record User(String username, String password) {
// empty body
}
Now our User
record has a username and a password variable, but where are the getters and the constructor? Those are automatically generated by the Java Compiler.
Now let's look at the class
implementation of this code:
public final class User {
private final String username;
private final String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String username() {
return username;
}
public String password() {
return password;
}
@Override
public boolean equals(Object obj) {
if (obj == this)
return true;
if (obj == null || obj.getClass() != this.getClass())
return false;
var that = (User) obj;
return Objects.equals(this.username, that.username) &&
Objects.equals(this.password, that.password);
}
@Override
public int hashCode() {
return Objects.hash(username, password);
}
@Override
public String toString() {
return "User[" +
"username=" + username + ", " +
"password=" + password + ']';
}
}
Here we can see the advantages of records: they reduce the need to write boilerplate code by a lot.
Custom constructors
While a constructor is automatically generated for us, we can still customize its implementation:
public record User(String username, String password) {
public User {
if (username == null || password == null) {
throw new IllegalArgumentException("Username and password must not be null");
}
}
}
Notice the way the constructor is written. Unlike a class constructor, a record constructor doesn't have a formal parameter list. Instead, it's just the access modifier followed by the name of the record and ending with curly brackets. This constructor type is called a compact constructor.
Custom getters
Getters in records are named after the variable they give back. In our example above, the getter name of username
would be username()
. Now we can customize these getters by simply repeating the same method signature and inserting our code in curly brackets:
public record User(String username, String password) {
public String username() {
return username.toUpperCase(Locale.ENGLISH);
}
}
Features and limitations
So what are the limitations of records? First of all, they can't extend a class. However, you can still declare them inside another class. They are also implicitly final, which means they can't be abstract and also can't be extended by any other class, but you can still implement interfaces with them. The next restriction is that you can't declare instance fields except for the ones in the record signature, which are also immutable. So once they are created you can't change the values of a record. It is still possible to have generic records and have static components (static methods, static fields, and initializers) as well as constructors and instance methods. They are also compatible with annotations.
Let's sum this up again. Records cannot:
- be abstract;
- extend classes;
- declare instance fields;
- be extended by classes.
On the other hand, records can:
- be declared inside a class;
- implement interfaces;
- be generic;
- be compatible with annotations;
- have static components;
- have constructors;
- have instance methods.
Conclusion
Records are a new way to declare a type in Java, similar to classes. Using records, we can reduce the boilerplate code of immutable data classes as most of the code is generated by the compiler. They are supported on the language level and therefore don't need any third-party dependencies to work.