1. 序章

Lombok は、Javaアプリケーションを作成する際のボイラープレートコードを大幅に削減するのに役立つライブラリです。

このチュートリアルでは、このライブラリを使用して、単一のプロパティのみに変更を加えた不変オブジェクトのコピーを作成する方法を説明します。

2. 使用法

設計上セッターを許可しない不変オブジェクトを操作する場合、現在のオブジェクトと同様のオブジェクトが必要になる場合がありますが、プロパティが1つだけ異なります。 これは、Lombokの@Withアノテーションを使用して実現できます。

public class User {
    private final String username;
    private final String emailAddress;
    @With
    private final boolean isAuthenticated;

    //getters, constructors
}

上記のアノテーションは、内部で以下を生成します。

public class User {
    private final String username;
    private final String emailAddress;
    private final boolean isAuthenticated;

    //getters, constructors

    public User withAuthenticated(boolean isAuthenticated) {
        return this.isAuthenticated == isAuthenticated ? this : new User(this.username, this.emailAddress, isAuthenticated);
    }
}

次に、上記で生成されたメソッドを使用して、元のオブジェクトの変更されたコピーを作成できます。

User immutableUser = new User("testuser", "[email protected]", false);
User authenticatedUser = immutableUser.withAuthenticated(true);

assertNotSame(immutableUser, authenticatedUser);
assertFalse(immutableUser.isAuthenticated());
assertTrue(authenticatedUser.isAuthenticated());

さらに、クラス全体に注釈を付けるオプションがあります。これにより、すべてのプロパティに対してwithX()メソッドが生成されます。

3. 要件

@With アノテーションを正しく使用するには、すべての引数のコンストラクターを提供する必要があります。 上記の例からわかるように、生成されたメソッドでは、元のオブジェクトのクローンを作成するためにこれが必要です。

この要件を満たすために、Lombok独自の@AllArgsConstructorまたは@Valueアノテーションを使用できます。 または、クラス内の非静的プロパティの順序がコンストラクターの順序と一致するようにしながら、このコンストラクターを手動で提供することもできます。

@Withアノテーションは、静的フィールドで使用された場合は何もしないことを覚えておく必要があります。 これは、静的プロパティがオブジェクトの状態の一部とは見なされないためです。 また、Lombokは、$記号で始まるフィールドのメソッドの生成をスキップします。

4. 高度な使用法

このアノテーションを使用する際のいくつかの高度なシナリオを調べてみましょう。

4.1. 抽象クラス

抽象クラスのフィールドで@Withアノテーションを使用できます。

public abstract class Device {
    private final String serial;
    @With
    private final boolean isInspected;

    //getters, constructor
}

ただし、生成されたwithInspected()メソッドの実装を提供する必要があります。 これは、Lombokが抽象クラスのクローンを作成するための具体的な実装について何も知らないためです。

public class KioskDevice extends Device {

    @Override
    public Device withInspected(boolean isInspected) {
        return new KioskDevice(getSerial(), isInspected);
    }

    //getters, constructor
}

4.2. 命名規則

上で特定したように、Lombokは$記号で始まるフィールドをスキップします。 ただし、フィールドが文字で始まる場合は、タイトルが大文字になり、最後に、生成されたメソッドの前にwithが付きます。

または、フィールドがアンダースコアで始まる場合は、生成されたメソッドの前にwithが付けられます。

public class Holder {
    @With
    private String variableA;
    @With
    private String _variableB;
    @With
    private String $variableC;

    //getters, constructor excluding $variableC
}

上記のコードによると、最初の2つの変数のみがwithX()メソッドを生成することがわかります。

Holder value = new Holder("a", "b");

Holder valueModifiedA = value.withVariableA("mod-a");
Holder valueModifiedB = value.with_variableB("mod-b");
// Holder valueModifiedC = value.with$VariableC("mod-c"); not possible

4.3. メソッド生成の例外

$ 記号で始まるフィールドに加えて、 LombokはwithX()メソッドがクラスにすでに存在する場合、それを生成しないことに注意してください。

public class Stock {
    @With
    private String sku;
    @With
    private int stockCount;

    //prevents another withSku() method from being generated
    public Stock withSku(String sku) {
        return new Stock("mod-" + sku, stockCount);
    }

    //constructor
}

上記のシナリオでは、新しい withSku()メソッドは生成されません。

さらに、Lombok skips 次のシナリオでのメソッド生成

public class Stock {
    @With
    private String sku;
    private int stockCount;

    //also prevents another withSku() method from being generated
    public Stock withSKU(String... sku) {
        return sku == null || sku.length == 0 ?
          new Stock("unknown", stockCount) :
          new Stock("mod-" + sku[0], stockCount);
    }

    //constructor
}

上記のwithSKU()メソッドの名前が異なっていることがわかります。

基本的に、Lombokは次の場合にメソッドの生成をスキップします。

  • 生成されたメソッド名と同じメソッドが存在します(大文字と小文字を区別しない)
  • 既存のメソッドには、生成されたメソッドと同じ数の引数があります(var-argsを含む)

4.4. 生成されたメソッドのヌル検証

他のLombokアノテーションと同様に、@Withアノテーションを使用して生成されたメソッドにnullチェックを含めることができます。

@With
@AllArgsConstructor
public class ImprovedUser {
    @NonNull
    private final String username;
    @NonNull
    private final String emailAddress;
}

Lombokは、必要なnullチェックとともに次のコードを生成します。

public ImprovedUser withUsername(@NonNull String username) {
    if (username == null) {
        throw new NullPointerException("username is marked non-null but is null");
    } else {
        return this.username == username ? this : new ImprovedUser(username, this.emailAddress);
    }
}

public ImprovedUser withEmailAddress(@NonNull String emailAddress) {
    if (emailAddress == null) {
        throw new NullPointerException("emailAddress is marked non-null but is null");
    } else {
        return this.emailAddress == emailAddress ? this : new ImprovedUser(this.username, emailAddress);
    }
}

5. 結論

この記事では、Lombokの @With アノテーションを使用して、単一のフィールドを変更した特定のオブジェクトのクローンを生成する方法を説明しました。

また、このメソッド生成が実際にいつどのように機能するか、およびnullチェックなどの追加の検証でメソッド生成を拡張する方法についても学びました。

いつものように、コード例はGitHubから入手できます。