Immutability with Lombok builder pattern

Lombok is a tool that makes it possible to write classes in Java the way it is supposed to. It reduces a lot of the boilerplate code required that many modern languages already have built-in support for, such as Golang and Kotlin. Lombok supports the traditional OOP class structure with getters and setters, but my favorite part is the Lombok Builder which enables us to write immutable classes with almost no code required at all. In this article, we are going to explore that and hopefully, you will be convinced and start using it yourself.

If you have been following my previous blogs, you might already know that I am a big advocate of immutable objects, it has tons of benefits, as you can read about more in detail in my previous article Achieve thread-safety with immutability. However, one of the downsides of the builder pattern is that it requires a lot more boilerplate code to get started (although, in my opinion, it is totally worth it). But wouldn’t it be nice if we could get all the good stuff that comes from immutability with the builder pattern, but at the same time don’t require so much boilerplate code? Well, with Lombok it is actually totally possible!

Maven dependency

We only need one dependency to get started with Lombok.


    org.projectlombok
    lombok

Lombok builder

Let’s create an example class. My class represents a Person that has a name, an age and lives in a city. The person may also have children and a car.


@Builder(toBuilder = true)
@ToString
@Getter
public class Person {

  @NonNull private final String name;
  @NonNull private final String city;
  @NonNull private final Integer age;
  private final List children;
  private final String car;

}

That’s all the code required for our immutable person class with the builder pattern, isn’t that awesome?! The @Builder annotation is the key here. With that annotation, it becomes possible to create a person like the following.

Person person = Person.builder()
    .name("Viktor")
    .city("Stockholm")
    .age(24)
    .build();

However, there isn’t that much that we can do with that person without adding a couple of more annotations such as; @Getter which adds getters for all of our fields, @ToString which generates a nice toString() with all of our fields.

Additionally, I have annotated some fields with @NonNull which will perform a null check in the constructor, and throw a NullPointerException when attempting to create an object with a not allowed null value. Personally, I would have preferred a IllegalArgumentException but if that is important for you, then you can simply override the constructor, creating a private one that takes all the arguments and where you can check for null and other values that you don’t want to allow.

Comparison

If we were to create the above class above manually, it would be a lot of boilerplate code and it would look something similar to the following.

public class ManualPerson {

  private final String name;
  private final String city;
  private final Integer age;
  private final List children;
  private final String car;

  private ManualPerson(String name, String city, Integer age, List children, String car) {
    if (name == null) {
      throw new NullPointerException("Name isn't allowed to be null");
    }
    if (city == null) {
      throw new NullPointerException("City isn't allowed to be null");
    }
    if (age == null) {
      throw new NullPointerException("Age isn't allowed to be null");
    }
    this.name = name;
    this.city = city;
    this.age = age;
    this.children = children;
    this.car = car;
  }

  public String getName() {
    return name;
  }

  public String getCity() {
    return city;
  }

  public Integer getAge() {
    return age;
  }

  public List getChildren() {
    return children;
  }

  public String getCar() {
    return car;
  }

  public static Builder builder() {
    return new Builder();
  }

  public Builder toBuilder() {
    return new Builder(this);
  }

  @Override
  public String toString() {
    return "ManualPerson(name=" + name
        + ", city=" + city
        + ", age=" + age
        + ", children=" + children
        + ", car=" + car
        + ")";
  }

  public static final class Builder {
    private String name;
    private String city;
    private Integer age;
    private List children;
    private String car;

    public Builder() {

    }

    public Builder(ManualPerson person) {
      this.name = person.name;
      this.city = person.city;
      this.age = person.age;
      this.children = person.children;
      this.car = person.car;
    }

    public Builder name(String name) {
      this.name = name;
      return this;
    }

    public Builder city(String city) {
      this.city = city;
      return this;
    }

    public Builder age(Integer age) {
      this.age = age;
      return this;
    }

    public Builder children(List children) {
      this.children = children;
      return this;
    }

    public Builder car(String car) {
      this.car = car;
      return this;
    }

    public Person build() {
      return new Person(name, city, age, children, car);
    }
  }
}

Around 100 more lines of code required to do the exact same thing, that’s a huge difference! As a lazy developer (which most of us are by nature), you gotta love that.

Testing the Lombok builder class

I’m sure we all can agree that Lombok looks nice in theory, but let’s get practical and put it to test to make sure that it works. I wrote a couple of JUnit tests that can be seen below.


public class BuilderTester {

  @Test
  public void testBuilderPattern() {
    Person person = Person.builder()
        .name("Joakim")
        .city("Stockholm")
        .age(48)
        .children(Arrays.asList("Viktor", "Jenny", "Thea"))
        .build();

    assertEquals("Person(name=Joakim, city=Stockholm, age=48, children=[Viktor, Jenny, Thea], car=null)", person.toString());
  }

  @Test (expected = NullPointerException.class)
  public void assertNameCantBeNull() {
    Person.builder()
        .age(18)
        .city("Stockholm")
        .build();
  }

  @Test
  public void assertToBuilder() {
    Person person = Person.builder()
        .name("Joakim")
        .city("Stockholm")
        .age(48)
        .build();

    Person person2 = person.toBuilder()
        .name("Karin")
        .age(50)
        .build();

    assertEquals("Joakim", person.getName());
    assertEquals("Karin", person2.getName());
  }

}

Other handy Lombok annotation

There are tons of more handy Lombok annotations such as @EqualsAndHashCode to generate an equals and hashcode with all of your fields, and @Synchronized which doesn’t expose your locks. I recommend checking out Project Lombok for additional features.

Final words

This was a quick introduction to Lombok, and more specifically the Lombok Builder. After discovering Lombok, it is hard to go back to plain old Java classes, and why should you? You will save tons of time from not wasting it on writing boilerplate code.

The only true downside that I see to Lombok is that all of your team members will need to enable it in their IDEA, or they will see lots of errors. But that’s the only real downside that I can see to it, of course, there are minor stuff such as @NonNull returning a NullPointerException instead of a IllegalArgumentException, but small stuff like that is easily worth to put up with when you get to skip writing boring boilerplate code.

Additionally, even though you add annotations, for example @Getter that doesn’t prevent you from still creating a custom getter for certain fields. Let’s say that you want to make use of Java 8 Optional, if you simply create a getter like the following:

public Optional getCar() {
    return Optional.ofNullable(car);
}

It then it overrides the getter that Lombok was going to create for you. Which is very convenient since it will make it a lot easier to start implementing Lombok in your project, but still maintain some custom stuff that you require.

One thought on “Immutability with Lombok builder pattern”

  1. Hi, you can make @NonNull return an IllegalArgumentException if you add the following line to your lombok.config file:

    lombok.nonNull.exceptionType = IllegalArgumentException

Leave a Reply