1. 概要

Lombokライブラリは、ボイラープレートコード@Builderアノテーションを記述せずにBuilderパターンを実装するための優れた方法を提供します。

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. 1つは標準のLombok機能に依存しています。 もう1つは、Lombok1.18で導入された実験的な機能を利用します。

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

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

2. ロンボク@Builderと継承

2.1. 問題の定義

ChildクラスがParentクラスを拡張するとします。

@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 that extends another class like that, we’ll get the following compilation error on the annotation:

暗黙のスーパーコンストラクターParent()は未定義です。 別のコンストラクターを明示的に呼び出す必要があります

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. 問題の解決

幸いなことに、簡単な回避策があります。 フィールドベースのコンストラクターを(IDEを使用して、または手動で)生成できます。 This also includes the fields from the superclasses.

クラスの代わりに@Builderで注釈を付けます。

@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 also specify 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. 複数の@Builderを共存させる

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

戻りタイプはParent.builder()と互換性がありません

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

この問題は、ビルダーメソッドの少なくとも1つに一意の名前を割り当てることで修正できます。

@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;
    }
}

これで、 ParentBuilderからChild.builder()および ChildBuilderからChild.childBuilder()を取得できるようになります。

2.4. より大きな継承階層のサポート

場合によっては、より深い継承階層をサポートする必要があります。 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;
    }
}

前と同じように、コンストラクターを手動で追加する必要があります。 これは、すべての親クラスと子からのすべてのプロパティを引数として受け入れる必要があります。 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 then extend this pattern to deal with any depth of inheritance. The constructor that we need to create can become quite large, but our IDE can help us out.

3. ロンボク@SuperBuilderと継承

前述したように、Lombokのバージョン1.18では@SuperBuilderアノテーションが導入されました。これを使用すると、問題をより簡単に解決できます。

3.1. 注釈の適用

祖先のプロパティを表示できるビルダーを作成できます。

これを行うために、クラスとその祖先に@SuperBuilderアノテーションを付けます。

Let’s demonstrate on our three-tier hierarchy here.

単純な親と子の継承の原則は同じであることに注意してください。

@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, we get a builder for the child class that exposes the properties of the parents too.

すべてのクラスに注釈を付ける必要があることに注意してください。 @SuperBuilderを同じクラス階層内で@Builderと混在させることはできません。そうすると、コンパイルエラーが発生します。

3.2. 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. 結論

継承を利用するクラスで@Builderアノテーションを使用する際の一般的な落とし穴に対処する方法を見てきました。

メインのLombok@Builder アノテーションを使用する場合、それを機能させるための追加の手順がいくつかあります。 But if we are willing to use the experimental features, @SuperBuilder can simplify things.

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