Java Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

The Lombok library provides a great way to implement the Builder Pattern without writing any boilerplate code: the @Builder annotation.

In this short tutorial, we're specifically going to learn how to deal with the @Builder annotation when inheritance is involved. We will demonstrate two techniques. One relies on standard Lombok features. The other makes use of an experimental feature introduced in Lombok 1.18.

For a wider overview of the Builder annotation, we can refer to Using Lombok’s @Builder Annotation.

A detailed look to the Project Lombok library is also available in Introduction to Project Lombok.

2. Lombok @Builder and Inheritance

2.1. Defining the Problem

Let's suppose our Child class extends a Parent class:

@Getter
@AllArgsConstructor
public class Parent {
    private final String parentName;
    private final int parentAge;
}

@Getter
@Builder
public class Child extends Parent {
    private final String childName;
    private final int childAge;
}

When using @Builder on a class which extends another class like that, we'll get the following compilation error on the annotation:

Implicit super constructor Parent() is undefined. Must explicitly invoke another constructor

This is due to the fact that Lombok doesn't take into account the fields of the superclasses, but only the ones from the current class.

2.2. Solving the Problem

Luckily for us, there's a simple workaround. We can generate (with our IDE or even manually) a field-based constructor. This includes also the fields from the superclasses. We annotate it with @Builder, instead of the class:

@Getter
@AllArgsConstructor
public class Parent {
    private final String parentName;
    private final int parentAge;
}

@Getter
public class Child extends Parent {
    private final String childName;
    private final int childAge;

    @Builder
    public Child(String parentName, int parentAge, String childName, int childAge) {
        super(parentName, parentAge);
        this.childName = childName;
        this.childAge = childAge;
    }
}

This way, we'll be able to access a convenient builder from the Child class, which will allow us to specify also the Parent class' fields:

Child child = Child.builder()
  .parentName("Andrea")
  .parentAge(38)
  .childName("Emma")
  .childAge(6)
  .build();

assertThat(child.getParentName()).isEqualTo("Andrea");
assertThat(child.getParentAge()).isEqualTo(38);
assertThat(child.getChildName()).isEqualTo("Emma");
assertThat(child.getChildAge()).isEqualTo(6);

2.3. Making Multiple @Builders Coexist

In case the superclass is itself annotated with @Builder, we'll get the following error when annotating the Child class' constructor:

The return type is incompatible with Parent.builder()

This is because the Child class is trying to expose both the Builders with the same name.

We can fix this problem by assigning a unique name to at least one of the builder methods:

@Getter
public class Child extends Parent {
    private final String childName;
    private final int childAge;
    
    @Builder(builderMethodName = "childBuilder")
    public Child(String parentName, int parentAge, String childName, int childAge) {
        super(parentName, parentAge);
        this.childName = childName;
        this.childAge = childAge;
    }
}

We'll then be able to obtain a ParentBuilder through Child.builder() and a ChildBuilder through Child.childBuilder().

2.4. Supporting Larger Inheritance Hierarchies

In some cases, we may need to support deeper inheritance hierarchies. We can make use of the same pattern as before. Let's create a subclass of Child.

@Getter
public class Student extends Child {

    private final String schoolName;

    @Builder(builderMethodName = "studentBuilder")
    public Student(String parentName, int parentAge, String childName, int childAge, String schoolName) {
        super(parentName, parentAge, childName, childAge);
        this.schoolName = schoolName;
    }
}

As before, we need to manually add a constructor. This needs to accept all properties from all the parent classes, and the child, as arguments. We then add the @Builder annotation as before. By providing another unique method name in the annotation, we can obtain builders for Parent, Child or Student.

Student student = Student.studentBuilder()
  .parentName("Andrea")
  .parentAge(38)
  .childName("Emma")
  .childAge(6)
  .schoolName("Baeldung High School")
  .build();

assertThat(student.getChildName()).isEqualTo("Emma");
assertThat(student.getChildAge()).isEqualTo(6);
assertThat(student.getParentName()).isEqualTo("Andrea");
assertThat(student.getParentAge()).isEqualTo(38);
assertThat(student.getSchoolName()).isEqualTo("Baeldung High School");

We can extend this pattern, then, to deal with any depth of inheritance. The constructor that we need to create can become quite large, but your IDE can help you out.

3. Lombok @SuperBuilder and Inheritance

As we noted earlier, version 1.18 of Lombok introduced the @SuperBuilder annotation. We can use this to solve our problem in a simpler way.

3.1. Applying the Annotations

We can make a builder that can see the properties of its ancestors.

To do this, we annotate our class and its ancestors with the @SuperBuilder annotation.

Let's demonstrate on our three-tier hierarchy here. Note that the principle for simple parent and child inheritance is the same:

@Getter
@SuperBuilder
public class Parent {
    // same as before...

@Getter
@SuperBuilder
public class Child extends Parent {
   // same as before...

@Getter
@SuperBuilder
public class Student extends Child {
   // same as before...

When all classes are annotated in this way, then we get a builder for the child class that exposes the properties of the parents, too.

Note that we have to annotate all classes. @SuperBuilder cannot be mixed with @Builder within the same class hierarchy. Doing so will result in a compilation error.

3.2. Using the Builder

This time, we don't need to define any special constructors. The builder class generated by @SuperBuilder behaves just like the one we generated using the main Lombok @Builder:

Student student = Student.builder()
  .parentName("Andrea")
  .parentAge(38)
  .childName("Emma")
  .childAge(6)
  .schoolName("Baeldung High School")
  .build();

assertThat(student.getChildName()).isEqualTo("Emma");
assertThat(student.getChildAge()).isEqualTo(6);
assertThat(student.getParentName()).isEqualTo("Andrea");
assertThat(student.getParentAge()).isEqualTo(38);
assertThat(student.getSchoolName()).isEqualTo("Baeldung High School");

4. Conclusion

We’ve seen how to deal with the common pitfalls of using the @Builder annotation in classes that make use of inheritance.

If we use the main Lombok @Builder annotation, we have a few extra steps to make it work. But if we are willing to use the experimental features, then @SuperBuilder can simplify things.

As always, the full source code is available over on Github.

Java bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
Comments are closed on this article!