1. Introduction
In this quick tutorial, we’ll investigate how can we provide default values for attributes when using the builder pattern with Lombok.
Make sure to check out our intro to Lombok as well.
2. Dependencies
We’ll use Lombok in this tutorial, so we only need one dependency:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
3. POJO With Lombok Builder
First, let’s have a look at how Lombok can help us get rid of the boilerplate code needed to implement the builder pattern.
We’ll start with a simple POJO:
public class Pojo {
private String name;
private boolean original;
}
For this class to be useful, we’ll need getters. Also, if we wish to use this class with an ORM, we’ll probably need a default constructor.
On top of these, we want a builder for this class. With Lombok, we can have all this with some simple annotations:
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Pojo {
private String name;
private boolean original;
}
4. Defining Expectations
Let’s define some expectations for what we want to achieve in the form of unit tests.
The first and most basic requirement is the presence of default values after we build an object with a builder:
@Test
public void givenBuilderWithDefaultValue_ThanDefaultValueIsPresent() {
Pojo build = Pojo.builder()
.build();
Assert.assertEquals("foo", build.getName());
Assert.assertTrue(build.isOriginal());
}
Of course, this test fails, since the @Builder annotation doesn’t populate values. We’ll fix this soon.
If we use an ORM, it usually relies on a default constructor. So we should expect the same behavior from the default constructor as we do from the builder:
@Test
public void givenBuilderWithDefaultValue_ThanNoArgsWorksAlso() {
Pojo build = Pojo.builder()
.build();
Pojo pojo = new Pojo();
Assert.assertEquals(build.getName(), pojo.getName());
Assert.assertTrue(build.isOriginal() == pojo.isOriginal());
}
At this stage, this test passes.
Now let’s see how we can make both tests pass.
5. Lombok’s Builder.Default Annotation
Since Lombok v1.16.16, we can use @Builder‘s inner annotation:
// class annotations as before
public class Pojo {
@Builder.Default
private String name = "foo";
@Builder.Default
private boolean original = true;
}
It’s simple and readable. The default values will be present with the builder, making the first test case pass.
However, if we work with Lombok prior to version 1.18.2, the no-args constructor won’t get the default values, making the second test case fail.
The Lombok team fixed this inconsistence problem in version 1.18.2. As the version we’re using in this tutorial is later than 1.18.2, both tests pass.
6. Initialize the Builder
We can try to make both tests pass by defining default values in a minimalistic builder implementation:
// class annotations as before
public class Pojo {
private String name = "foo";
private boolean original = true;
public static class PojoBuilder {
private String name = "foo";
private boolean original = true;
}
}
This way, both tests will pass.
Unfortunately, the price is code duplication. For a POJO with tens of fields, it could be error prone to maintain the double initialization.
But if we’re willing to pay this price, we should take care of one more thing, too. If we rename our class using a refactoring within our IDE, the static inner class won’t be automatically renamed. Then Lombok won’t find it and our code will break.
To eliminate this risk, we can decorate the builder annotation:
// class annotations as before
@Builder(builderClassName = "PojoBuilder")
public class Pojo {
private String name = "foo";
private boolean original = true;
public static class PojoBuilder {
private String name = "foo";
private boolean original = true;
}
}
7. Using toBuilder
@Builder also supports generating an instance of the builder from an instance of the original class. This feature isn’t enabled by default. We can enable it by setting the toBuilder parameter in the builder annotation:
// class annotations as before
@Builder(toBuilder = true)
public class Pojo {
private String name = "foo";
private boolean original = true;
}
With this, we can get rid of the double initialization.
Of course, there’s a price for that. We have to instantiate the class to create a builder. So we have to modify our tests also:
@Test
public void givenBuilderWithDefaultValue_ThanDefaultValueIsPresent() {
Pojo build = new Pojo().toBuilder()
.build();
Assert.assertEquals("foo", build.getName());
Assert.assertTrue(build.isOriginal());
}
@Test
public void givenBuilderWithDefaultValue_ThanNoArgsWorksAlso() {
Pojo build = new Pojo().toBuilder()
.build();
Pojo pojo = new Pojo();
Assert.assertEquals(build.getName(), pojo.getName());
Assert.assertTrue(build.isOriginal() == pojo.isOriginal());
}
Again, both tests pass, so we have the same default value using the no-args constructor as when using the builder.
8. Conclusion
In this article, we explored several options to provide default values for the Lombok builder.
The side effect of the Builder.Default annotation is worth keeping an eye on. But the other options also have their drawbacks, so we have to choose carefully based on the current situation.
As always, the code is available over on GitHub.