Java Records is a new feature introduced in Java 14 to make it easier for developers to create immutable classes. A record is a special type of class (and a new keyword too) that acts as a data container or a data object, and it's used to hold a fixed set of values.
Let's check a short Java Records example.
record Point(int x, int y) {}
This creates a new record class called Point, with two fields x and y. These fields are automatically marked as final, so they cannot be modified once the record is created.
Under the hood a few methods are going to be generated to help you, such as:
- A constructor that initializes all the fields
- Accessor methods (getters and setters) for each field
- And toString, hashCode, and equals methods
This means that you don't have to write boilerplate code for these common tasks, making it much easier to create and maintain your code. And also by using Java Records you can eliminate a lot of use cases for the loved and hated Lombok library.
Why use Records?
The short answer is Records are more expressive, readable and faster than traditional DTO classes.
Using the example we already presented above, of a point in 2D space, you can easily use a record to clearly express that the class only has two fields and nothing else. Showing how readable and expressive record classes are.
DTOs vs Record
This is a inevitable comparison even though DTOs are not the only use case for Java Records. Both are used to hold a fixed set of values, but they have some key differences. When we compare Records with a DTO stylish class we can see a big difference in expressiveness and readability.
We can replace about 40 lines of code with 1 line only. Check the Point class written using the DTO pattern.
class PointDTO {
private int x;
private int y;
public PointDTO(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override
public String toString() {
return "PointDTO{" +
"x=" + x +
", y=" + y +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PointDTO pointDTO = (PointDTO) o;
return x == pointDTO.x &&
y == pointDTO.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
As you can see, compared to the Record class, the DTO class contains a lot more code (boilerplate code), including the constructor, getter methods, and the methods toString, hashCode, and equals which are common to most DTOs.
- Unlike DTOs, records are immutable by nature, meaning their fields cannot be modified once the record is created. This makes records more suitable for functional programming and improves code safety.
- Records have built-in support for common methods like toString, hashCode, and equals, which eliminates the need for boilerplate code.
- Records are a more modern and expressive way of creating data classes compared to DTOs, which could make it easier for new developers to read and understand the codebase.
And we can point out more reasons to use Records.
JVM loves Records (and vice versa)
Java Records are more efficient than traditional classes in terms of memory usage and performance because of their immutable nature, the way they are implemented in the JVM, and the techniques like compacting and inlining that the JVM can apply to classes markes as records.
Records eliminate the need for defensive copying (check Guideline 6-2), which can improve performance by reducing the number of operations that need to be performed on the data.
Because Records don't have any additional methods or fields, the JVM can use more aggressive optimization techniques for records, like inlining (check Phase 1), that can further improve performance. But, it doesn't mean more methods can be added, you still can adding more methods as you need.
Disadvantages of using Records
As with everything in life (technology especially), there are also some drawbacks when using records.
Java Records are not suitable for use as JPA (Java Persistence API) entity classes because they have some limitations that make them incompatible with the requirements of JPA entities.
- JPA entities are required to have a no-arg constructor, which is not provided by default for records. This means that you would have to manually add a no-arg constructor to the record class, which would defeat the purpose of using a record in the first place.
- JPA entities are required to have setter methods for each field so that the entity manager can update the fields in the database. However, records do not have setter methods by default, as their fields are automatically marked as final.
- JPA entities can have additional methods and fields that are not part of the record.
- JPA entities can be extended or implemented interfaces or classes, while records cannot.
As you can see while records provide a concise and efficient way to create immutable classes, they are not suitable for use as JPA entities due to the limitations on constructors, setter methods, additional methods, fields and interfaces or classes that JPA entities require.
To wrap up
When I first saw Records my thought was to replace all of my DTOs and JPA Entities with Records right away but I was partially wrong. Clearly, DTOs and data classes are good candidates to be replaced by Records but not our lovely and long JPA Entities because of the limitation we discussed here.
One more time writing and diving into concepts I like such as Records made me more confident to make choices about whether recommend or not Records for my Java projects or even better to refactor or not to use Records. And due to the simple use and low cost of Records it can be used in different parts of a Java codebase and not blocking you to use other type of classes such as Entities in different parts of the code base.
What about you? Are you using Records in your projects? How is the experience so far?
Top comments (4)
Well you don't really need empty constructor for JPA. I've managed to have class without such constructors by using@ConstructorBinding
when using with Spring (and Java 1.8). I didn't invest too much into the records since project was not yet upgraded to Java 17 but there are ways that Spring will support to have immutable data. Also class didn't have setters since they weren't needed due to constructor handling settings part but they did have getters. Not sure is it possible to annotate somehow record in Spring to enable simple read write. I'll probably give it a try in a test project with Spring Boot and Java 17 just to see but I'm mostly interested because my class was fine for read and write although you had to remember when hitting.save(x)
it would require to set new variable to result of that method due to ID being null and having no setter so spring would create a new object.Update: Latest test with Java 17 and Spring Boot 2.7.7 shows that it's required to have empty constructor (even private one is allowed). If fields are final Spring will utilize unsafe (not sure) parts and set these values.
On Java 1.8 with Spring 2.6 or such (don't have access to the code anymore) it was clear in debugging that instances were different when saving: I've checked memory addresses at that time and ingoing variable had different address then output of .save as well as null being in input for ID while outgoing was fine. But now, in new project it shows same instance.
So couple of problems with Java 17 + Spring 2.7.7:
It's weird how Spring will work with private constructor and play with finals but refuses to mediate when such is missing. I'm not 100% sure what happened on Java 1.8 and Spring 2.6.14 that gave me different object instances when saving this weird class but I can now clearly see that regardless of what it was it cannot be applied to Records anyway.
It's a bit disappointing because if you used something like Micronaut you could avoid reflection and see how constructor detection would be quite easy but JPA specs actually require empty constructor so it would be breaking the pattern. Problem is Spring already does it to some extent so I wonder why wouldn't it allow to simply hold key-value properties and detect annotations then try to fill it out. I know that save and fetch don't behave the same way but still if it can ignore private & final it should be able to play with constructor bindings in the background as well. I mean looking at
@MappedEntity
example in Micronaut docs one can see that setter is given for Id because later it will be filled for the same instance - however this is different from JPA and doesn't use Hibernate.Great post, @felixcoutinho!!! Yeah, this feature brings a lot of capabilities for java development!!! Immutability and DTO are just the iceberg tip... features like Records and Pattern Matching together help us to explore a new paradigm: Data-oriented programming... Also, I hope that some initiatives to use Records with JPA become true as well!
Congratulations!!!
I use Records solely for DTOs
Thanks for this article.