覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。 twitter: @yyoshikaw

Lombok - @Wither

Lombokの@Wither(lombok.experimental.Wither)アノテーションの利用サンプルです。

Immutableなクラスでは、クラス内のメンバの値を変更したい場合にwithXXXXというメソッドを定義することがあります。
withメソッドは、元のインスタンスの内容は変更せず、指定の値を持つ新しいインスタンスを返します。
(例えば、LocalDateクラスのLocalDate#withYear 等がこれに該当します)

@Witherアノテーションを利用することでこのようなwithメソッドを簡単に定義することができます。
基本的に@Valueアノテーションと組み合わせて利用するのが良いと思います。

@Witherは所属パッケージが"experimental"になっており、その名の通り実験的なアノテーションです。
そのため、今後Lombokのバージョンが上がった際などに仕様が変更される可能性もあると思われます。


クラスに@Witherを付与する

クラスに@Witherアノテーションを付与すると、クラス内の各メンバに対応するwithメソッドが定義されます

■@Witherを付与したクラス

import lombok.Value;
import lombok.experimental.Wither;

@Value
@Wither
public class Person1 {

    private final long id;

    private final String name;

    private final String remarks;
}

■利用側のコード

全メンバへに対するwithメソッドが利用できることが分かります。

public final class Person1Client {

    public static void main(String[] args) {
        long id = 1;
        String name = "Smple Taro";
        String remarks = "Sample User";

        Person1 srcPerson = new Person1(id, name, remarks);

        System.out.println("Origianl Person: " + srcPerson);

        System.out.println("------------------");
        Person1 newPerson1 = srcPerson.withId(1000L);
        Person1 newPerson2 = srcPerson.withName("Ssmple Jiro");
        Person1 newPerson3 = srcPerson.withRemarks("dummy User");

        System.out.println("Origianl Person: " + srcPerson);
        System.out.println("New Person1: " + newPerson1);
        System.out.println("New Person2: " + newPerson2);
        System.out.println("New Person3: " + newPerson3);

        System.out.println("srcPerson == newPerson1 : " + (srcPerson == newPerson1));
        System.out.println("newPerson1 == newPerson2 : " + (newPerson1 == newPerson2));
        System.out.println("newPerson2 == newPerson3 : " + (newPerson2 == newPerson3));
    }
}

■実行結果

withXXXメソッドによって値を変更した場合、戻り値のインスタンスは元のインスタンスとは別物になっていることが分かります。

Origianl Person: Person1(id=1, name=Smple Taro, remarks=Sample User)
------------------
Origianl Person: Person1(id=1, name=Smple Taro, remarks=Sample User)
New Person1: Person1(id=1000, name=Smple Taro, remarks=Sample User)
New Person2: Person1(id=1, name=Ssmple Jiro, remarks=Sample User)
New Person3: Person1(id=1, name=Smple Taro, remarks=dummy User)
srcPerson == newPerson1 : false
newPerson1 == newPerson2 : false
newPerson2 == newPerson3 : false

■実際に生成されるソースコード

各メンバに対応するwithメソッドが生成されていることが分かります。

public final class Person1 {
    private final long id;
    private final String name;
    private final String remarks;

    public Person1(final long id, final String name, final String remarks) {
        this.id = id;
        this.name = name;
        this.remarks = remarks;
    }

    public long getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    public String getRemarks() {
        return this.remarks;
    }

    @Override
    public boolean equals(final Object o) {
        if (o == this) return true;
        if (!(o instanceof Person1)) return false;
        final Person1 other = (Person1) o;
        if (this.getId() != other.getId()) return false;
        final Object this$name = this.getName();
        final Object other$name = other.getName();
        if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false;
        final Object this$remarks = this.getRemarks();
        final Object other$remarks = other.getRemarks();
        if (this$remarks == null ? other$remarks != null : !this$remarks.equals(other$remarks)) return false;
        return true;
    }

    @Override
    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        final long $id = this.getId();
        result = result * PRIME + (int) ($id >>> 32 ^ $id);
        final Object $name = this.getName();
        result = result * PRIME + ($name == null ? 43 : $name.hashCode());
        final Object $remarks = this.getRemarks();
        result = result * PRIME + ($remarks == null ? 43 : $remarks.hashCode());
        return result;
    }

    @Override
    public String toString() {
        return "Person1(id=" + this.getId() + ", name=" + this.getName() + ", remarks=" + this.getRemarks() + ")";
    }

    public Person1 withId(final long id) {
        return this.id == id ? this : new Person1(id, this.name, this.remarks);
    }

    public Person1 withName(final String name) {
        return this.name == name ? this : new Person1(this.id, name, this.remarks);
    }

    public Person1 withRemarks(final String remarks) {
        return this.remarks == remarks ? this : new Person1(this.id, this.name, remarks);
    }
}


@Witherをメンバに付与する

値の変更を許可するメンバが限定したい状況も考えられます。
そのような場合は、@Witherアノテーションを値の変更を許容するメンバに付与します。

■@Witherを付与したクラス

メンバのnameとremarksに@Witherを付与しています

import lombok.Value;
import lombok.experimental.Wither;

@Value
public class Person2 {

    private final long id;

    @Wither
    private final String name;

    @Wither
    private final String remarks;
}

■実際に生成されるソースコード

nameとremarksに対応するwithメソッドは定義されていますが、idに対応するwithメソッドは定義されていなことが分かります

public final class Person2 {
    private final long id;
    private final String name;
    private final String remarks;

    public Person2(final long id, final String name, final String remarks) {
        this.id = id;
        this.name = name;
        this.remarks = remarks;
    }

    public long getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    public String getRemarks() {
        return this.remarks;
    }

    @Override
    public boolean equals(final Object o) {
        if (o == this) return true;
        if (!(o instanceof Person2)) return false;
        final Person2 other = (Person2) o;
        if (this.getId() != other.getId()) return false;
        final Object this$name = this.getName();
        final Object other$name = other.getName();
        if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false;
        final Object this$remarks = this.getRemarks();
        final Object other$remarks = other.getRemarks();
        if (this$remarks == null ? other$remarks != null : !this$remarks.equals(other$remarks)) return false;
        return true;
    }

    @Override
    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        final long $id = this.getId();
        result = result * PRIME + (int) ($id >>> 32 ^ $id);
        final Object $name = this.getName();
        result = result * PRIME + ($name == null ? 43 : $name.hashCode());
        final Object $remarks = this.getRemarks();
        result = result * PRIME + ($remarks == null ? 43 : $remarks.hashCode());
        return result;
    }

    @Override
    public String toString() {
        return "Person2(id=" + this.getId() + ", name=" + this.getName() + ", remarks=" + this.getRemarks() + ")";
    }

    public Person2 withName(final String name) {
        return this.name == name ? this : new Person2(this.id, name, this.remarks);
    }

    public Person2 withRemarks(final String remarks) {
        return this.remarks == remarks ? this : new Person2(this.id, this.name, remarks);
    }
}


withメソッドのアクセスレベルを制御する

@Witherを付与するとデフォルトで生成されるwithメソッドのアクセスレベルはpublicになります。
このアクセスレベルを変更したい場合は、パラメータにAccessLevelを指定します。

■@Witherを付与したクラス

nameに対する@WitherでのアクセスレベルにAccessLevel.PROTECTEDを指定しています。

import lombok.AccessLevel;
import lombok.Value;
import lombok.experimental.Wither;

@Value
public class Person3 {

    private final long id;

    @Wither(AccessLevel.PROTECTED)
    private final String name;

    @Wither
    private final String remarks;
}

■実際に生成されるソースコード

withName(nameに対するwithメソッド)のアクセスレベルがprotectedになっていることが分かります。

public final class Person3 {
    private final long id;
    private final String name;
    private final String remarks;

    public Person3(final long id, final String name, final String remarks) {
        this.id = id;
        this.name = name;
        this.remarks = remarks;
    }

    public long getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    public String getRemarks() {
        return this.remarks;
    }

    @Override
    public boolean equals(final Object o) {
        if (o == this) return true;
        if (!(o instanceof Person3)) return false;
        final Person3 other = (Person3) o;
        if (this.getId() != other.getId()) return false;
        final Object this$name = this.getName();
        final Object other$name = other.getName();
        if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false;
        final Object this$remarks = this.getRemarks();
        final Object other$remarks = other.getRemarks();
        if (this$remarks == null ? other$remarks != null : !this$remarks.equals(other$remarks)) return false;
        return true;
    }

    @Override
    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        final long $id = this.getId();
        result = result * PRIME + (int) ($id >>> 32 ^ $id);
        final Object $name = this.getName();
        result = result * PRIME + ($name == null ? 43 : $name.hashCode());
        final Object $remarks = this.getRemarks();
        result = result * PRIME + ($remarks == null ? 43 : $remarks.hashCode());
        return result;
    }

    @Override
    public String toString() {
        return "Person3(id=" + this.getId() + ", name=" + this.getName() + ", remarks=" + this.getRemarks() + ")";
    }

    protected Person3 withName(final String name) {
        return this.name == name ? this : new Person3(this.id, name, this.remarks);
    }

    public Person3 withRemarks(final String remarks) {
        return this.remarks == remarks ? this : new Person3(this.id, this.name, remarks);
    }
}


nullの設定を許可しないようにする

@Witherを付与して生成されるwithメソッドは、その引数にnullを指定できてしまいます。
nullを許可したくない場合は@NonNullを組み合わせましょう。

■@Witherを付与したクラス

nameに対して@Witherと@NonNullを指定しています。

import lombok.NonNull;
import lombok.Value;
import lombok.experimental.Wither;

@Value
public class Person4 {

    private final long id;

    @Wither
    @NonNull
    private final String name;

    private final String remarks;
}

■実際に生成されるソースコード

withName(nameに対するwithメソッド)で引数のnullチェックが行われていることが分かります。

import lombok.NonNull;

public final class Person4 {
    private final long id;
    @NonNull
    private final String name;
    private final String remarks;

    public Person4(final long id, @NonNull final String name, final String remarks) {
        if (name == null) {
            throw new NullPointerException("name");
        }
        this.id = id;
        this.name = name;
        this.remarks = remarks;
    }

    public long getId() {
        return this.id;
    }

    @NonNull
    public String getName() {
        return this.name;
    }

    public String getRemarks() {
        return this.remarks;
    }

    @Override
    public boolean equals(final Object o) {
        if (o == this) return true;
        if (!(o instanceof Person4)) return false;
        final Person4 other = (Person4) o;
        if (this.getId() != other.getId()) return false;
        final Object this$name = this.getName();
        final Object other$name = other.getName();
        if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false;
        final Object this$remarks = this.getRemarks();
        final Object other$remarks = other.getRemarks();
        if (this$remarks == null ? other$remarks != null : !this$remarks.equals(other$remarks)) return false;
        return true;
    }

    @Override
    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        final long $id = this.getId();
        result = result * PRIME + (int) ($id >>> 32 ^ $id);
        final Object $name = this.getName();
        result = result * PRIME + ($name == null ? 43 : $name.hashCode());
        final Object $remarks = this.getRemarks();
        result = result * PRIME + ($remarks == null ? 43 : $remarks.hashCode());
        return result;
    }

    @Override
    public String toString() {
        return "Person4(id=" + this.getId() + ", name=" + this.getName() + ", remarks=" + this.getRemarks() + ")";
    }

    public Person4 withName(@NonNull final String name) {
        if (name == null) {
            throw new NullPointerException("name");
        }
        return this.name == name ? this : new Person4(this.id, name, this.remarks);
    }
}



関連エントリ