Records in Java - what about Lombok?
Update April 2021: Please note this post is about Java 15, with records as preview feature. Things have changed, the update is available in another post.
Records: confrontation
When explaining Java records online I was asked several times ‘right, what about Lombok?’
In particular this question was ‘if the records are immutable, how are they different from Lombok’s @Value
?’
First thing one needs to remember is that “records are not Java Beans” and the @Value
gives us immutable JavaBeans.
The results of using record
and @Value
in code are sometimes very similar (if not the same).
equals()
,hashCode()
,toString()
are generated.- All fields are marked as
private final
. - The ‘all fields constructors’ are generated (if the Bean annotated with
@Value
has a field initialised during the declaration, then of course the field is skipped). record
and@Value
result in classes which arefinal
, so it’s impossible to inherit from them.
There are two fundamental differences:
- In records the components are declared in the record’s header (it’s beautifully described by the grammar from JEP 384), whereas in
@Value
d JavaBeans all fields need to be added and based on them the “canonical” constructor is being generated. - Records have accessors,
@Value
will give us getters.
Of course, there are more differences. If you’re really interested in them, the ultimate way to learn them is to compare the bytecode or at least to “deLombok” the code in your IDE. E.g., if the Lombok’s configuration in lombok.config
also has lombok.anyConstructor.addConstructorProperties=true
, then the “canonical” constructor thanks to @Value
is also annotated @java.beans.ConstructorProperties
. ‘Yeah, but it’s a minor detail…’. True, but this detail makes deserialization using Jackson from JSON to objects more elegant. As ‘elegant’ I mean without putting @JsonCreator
above the constructor and @JsonProperty("name)
before each constructor’s parameter. (We shall not forget that in the records the ‘normal deserialization’ from the binary format is also handled by the constructor, so the concept of ‘let’s deserialize using constructors’ gets more and more popular. Finally.)
There might be more differences in the future. One of the expected ways of using records is pattern matching with data extraction. And such extraction requires deconstructing the objects (like unapply
in Sala). One day we might find out that record
can also create such deconstructors for records as well. For time being I can’t tell if and how it’s going to work, if such deconstructors could be ‘manual’ as well and if Lombok is going to address that anyhow.
And now: kiss!
@Value
’s documentation (because you do RTFM, right?) notices, that in a way the @Value
is a shortcut for a several other annotations. The list looks like this: final @ToString @EqualsAndHashCode @AllArgsConstructor @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @Getter
Records have toString()
, so they don’t need @ToString
, and so on. Actually they have everything execept @Getter
.
What happens when we add @Getter
to a record’s declaration
@Getter
public record RecordWithGetters(int f1, String f2) {}
.class
file , which (after decompilation) looks more or less like this :
1 // IntelliJ API Decompiler stub source generated from a class file
2 // Implementation of methods is not available
3
4public final class RecordWithGetters extends java.lang.Record {
5 private final int f1;
6 private final java.lang.String f2;
7
8 public RecordWithGetters(int f1, java.lang.String f2) { /* compiled code */ }
9
10 public int getF1() { /* compiled code */ }
11
12 public java.lang.String getF2() { /* compiled code */ }
13
14 public java.lang.String toString() { /* compiled code */ }
15
16 public final int hashCode() { /* compiled code */ }
17
18 public final boolean equals(java.lang.Object o) { /* compiled code */ }
19
20 public int f1() { /* compiled code */ }
21
22 public java.lang.String f2() { /* compiled code */ }
23}
Meaning: beside the accessors of components, the records also has getters now. I don’t know if I like this. But I know that thanks to that we can add one more option to the list in the post on records' reflection and have the getters generated (instead of adding them manually), because ‘sadly, this pesky library needs getters’.
Right, but I mentioned @AllArgsConstructor
earlier… When we torture the records even more and add this annotation (and when we have the proper entry in json.config
):
@AllArgsConstructor
public record RecordWithAllArgsConstructor(int f1, String f2) {}
then after the compilation this is how the constructor looks like:
@java.beans.ConstructorProperties({"f1", "f2"})
public RecordWithAllArgsConstructor(int f1, java.lang.String f2) { /* compiled code */ }
What does it give us? Some “compatibility” with the Jackson when turning JSON into records (so deserializing).
Wrapping up: adding two more annotations @Getter @AllArgsConstructor
to a record’s definition and having properly configured Lombok in the project, will let us use the records in quite popular aspect, which is turning REST requests into Java objects/records and the objects/records into REST responses.
Is it worth it?
Honestly? I can’t tell.
All right, this would allow (if someone is not afraid of --enable-preview
and living on the edge) using records in a very popular area right now. And when all the libraries used by us will be aware of the record
s, we’d be able to get rid of these annotations.
However, Lombok doesn’t come for free. It’s difficult to use it without an IDE plugin, the plugin is not always up-to-date, Lombok (because of being a kind of “compiler plugin”) doesn’t work e.g. in mixed compilation with Scala, etc. It’s a huge disadvantage that using my current setup (Intellij IDEA 2020.1.1, Lombok plugin to IDEA: 0.30-2020.1) it’s impossible to call the record’s constructor if this constructor was generated by Lombok ;-) Compiling and running using Maven and OpenJDK Runtime Environment (build 15-ea+23-1098)
works.

So, is this technically possible? Yes. Is it worth it? You should answer this question yourself.
What about @Value?
‘All right, so if the @Getter @AllArgsConstructor
are in a way a subset of the @Value
, maybe we could use @Value
for records?’
This is something I wouldn’t do in any circumstances.
- Using
@Value
generates warning in the IDE, that the methodsequals()
andhashCode()
don’t take into account classes our class inherits from. At the first glance@Value record ARecord(){}
doesn’t inherit anything, but we shall not forgetjava.lang.Record
. Lombok, it seems, does for now. - When using Lombok and records at the same time it’d be nice to be careful about the method ‘overlapping’. If a method is being generated by Lombok and by
record
, then in the.class
file we’ll find the one generated by Lombok. And I think it’d better to have the ‘original record’ method. It’s a matter of taste and forward compatibility.
Lombok is a powerful tool, and as such it requires understanding and responsibility for all (mis)usages.
If your fingers are itchy, and you’d like to torture records on your own, the code is on Github.