Language selector

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 are final, 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 @Valued 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!

Now kiss
Careful readers of @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) {}
and compile that using Lombok as well? We’re going to have a .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 records, 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.

Lombok-generated constructor in IDEA's plugin

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 methods equals() and hashCode() 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 forget java.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.

Language selector