Pulog

JavaのLombokの getter / setter の挙動を再履修する

LombokはJava案件でよく利用しているのだが、特に @Getter / @Setter の挙動を特に理解せずに利用していた節があった。

なので、今回は実際にアノテーションを付与した状態でビルドし、デコンパイルした際にどのようなソースになっているのか、確認してみようと思います。

また、記事タイトルは getter / setter と書いていますが、getterとsetterも付与される @Data アノテーションの挙動も合わせて確認をしてみます。

環境の準備

とりあえず、以下2つが用意されていればLombokが使える環境をサクッと作成できる。
今回はGradleでLombokが使えるシンプルなプロジェクトを作成した。

  • IDE IntelliJ IDEA
  • JDK Amazon Corretto 11

IntelliJ IDEAで新規Gradleプロジェクトを作成後、 build.gradle を以下のように追記してビルドをすればOK。

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
+   annotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.12'
+   compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.12'
}

@Getter

まずはLombok入れるなら確実に使うであろう @Getter から。

ソースコード

ただメンバ変数だけを定義したクラスファイルに @Getter アノテーションを付与する。

import lombok.Getter;

@Getter
public class LombokExample {
    public String name;
    public Integer age;
    public Boolean leftyFlg;
}

ビルド後、デコンパイルした結果

メンバ変数全てに get[先頭大文字なメンバ変数名] の規則なgetterが生成された。

getterのアクセス修飾子が全て public になっているが、メンバ変数に合わせて生成されるわけではなく、 @Getter アノテーションのアクセスレベルのデフォルト値が AccessLevel.PUBLIC となっているため、publicメソッドとなっている。

public class LombokExample {
    public String name;
    public Integer age;
    public Boolean leftyFlg;
    public String getName() {
        return this.name;
    }
    public Integer getAge() {
        return this.age;
    }
    public Boolean getLeftyFlg() {
        return this.leftyFlg;
    }
}

一部だけgetterメソッドをoverwriteしたい場合

特に意識せずLombokで生成される同じメソッド名でカスタマイズしたメソッドを実装すれば、それをLombokが上書きすることはない。

ソース
@Getter
public class LombokExample {
    public String name;
    public String getName(){
        return this.name + "aaaa";
    }
}
ビルド後、デコンパイルした結果

Lombokのgetterで上書きされることはない。

public class LombokExample {
    public String name;
    public String getName() {
        return this.name + "aaaa";
    }
}

戻り値がメンバ変数と違う型のgetterを定義した場合

あまり考えたことなかった……
今回の実際にビルドしてデコンパイルしてよかったと思ううちの1つ。
と一瞬思ったけど、overloadでもなんでも無いので、Stringが戻り値なgetterも生成されることもない(そりゃそうですわ……)。

ソース
@Getter
public class LombokExample {
    public String name;
    // 文字列ではなく、数値を返す
    public Integer getName(){
        return Integer.parseInt(this.name);
    }
}
ビルド後、デコンパイルした結果

上記で説明したとおり、戻り値がStringなgetNameで勝手に上書きされることはない。

public class LombokExample {
    public String name;
    public Integer getName() {
        return Integer.valueOf(Integer.parseInt(this.name));
    }
}

一部だけアクセス修飾子を変更したい場合

ソース

基本的に全体的にpublic修飾子なgetterだが、一部だけprotectedにしたい場合はメンバ変数にも @Getter を指定し、アクセスレベルを指定してあげれば良い。

import lombok.AccessLevel;
import lombok.Getter;

@Getter
public class LombokExample {
    public String name;
    @Getter(AccessLevel.PROTECTED)
    protected Integer age;
}
ビルド後、デコンパイルした結果

この実装を見て分かる通り、classに @Getter アノテーションをつけなければ、 age だけgetterメソッドを実装することもできる。

全体的にgetterメソッドを生成したいのであればclassにアノテーションを付け、一部だけgetterメソッドを生成したいのであればメンバ変数単位でアノテーションを付与すればOK。

public class LombokExample {
    public String name;
    protected Integer age;
    public String getName() {
        return this.name;
    }
    protected Integer getAge() {
        return this.age;
    }
}

また、全体的にgetterメソッド生成したいが、一部のメンバ変数だけgetterメソッドを生成したくない場合、メンバ変数に @Getter(AccessLevel.NONE) を指定してあげればgetterメソッドは生成されない(実装は省略)。

@Setter

@Getter と同様に使われる @Setter ですが、挙動は @Getter と挙動はほぼ同じなので、簡単にだけ紹介してきます。

ソースコード

import lombok.AccessLevel;
import lombok.Setter;

@Setter
public class LombokExample {
    public String name;
    public Integer age;
    @Setter(AccessLevel.NONE)
    public Boolean leftyFlg;
}
ビルド後、デコンパイルした結果
public class LombokExample {
    public String name;
    public Integer age;
    public Boolean leftyFlg;
    public void setName(String name) {
        this.name = name;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
}

@Data

@Getter@Setter の2つのアノテーションを付与するのを省略するために @Data アノテーションを付与することも多いのですが、他にも色々なメソッドが提供されていそうなので、実際にビルドして確認してみます。

ソースコード

この時点で気付かれるかと思いますが、 final 修飾子がついている初期値が未定義なメンバ変数があるのにIDEがシンタックスエラーを出してきません。

実際にビルドされた後のソースを確認してみましょう。

import lombok.AccessLevel;
import lombok.Data;
import lombok.Setter;

@Data
public class LombokExample {
    public final String name;
    public Integer age;
    @Setter(AccessLevel.NONE)
    public Boolean leftyFlg;
}
ビルド後、デコンパイルした結果

前述の通り、確かに @Getter 及び @Setter アノテーションを付与した際と同じgetter / setterメソッドは生成されていそうですが、他にも色々なメソッドが生成されていそうです。

final 修飾子がついて未定義なメンバ変数がある場合は、そのメンバ変数をコンストラクタで初期化するようになっています。これはLombokの @RequiredArgsConstructor アノテーションを付与したのと同じといえます。

他にもインスタンス同士が同じかどうかを判定するために、 equalshashCode メソッドが生成されます。これはLombokの @EqualsAndHashCode アノテーションを付与したのと同じです。

あとはインスタンスのメンバ変数を出力してくれる toString メソッドも生成されます。これもLombokの @ToString アノテーションを付与したのと同じ効果です。

通常 System.out.println({インスタンス}) でインスタンスを出力しようとすると {クラス名}@{16進数な数値} が出力されてしまい、メンバ変数の値が出力されないのですが、 toString メソッドが生成されて、メンバ変数の値も可視化してくれるという便利なメソッドです。

本当にgetter / setterだけ生成したい場合は @Getter@Setter アノテーションを付与する必要がありそうです。

また、 @Data アノテーションの場合でも AccessLevel.NONE@Getter 及び @Setter アノテーションを付与すれば、そのメンバ変数のgetterまたはsetterメソッドは生成されないことが確認できると思います。

まとめると、 @Data アノテーションは以下アノテーションを付与したのと同じと言えます。

  • @EqualsAndHashCode
  • @Getter
  • @Setter
  • @RequiredArgsConstructor
  • @ToString
public class LombokExample {
    public final String name;
    public Integer age;
    public Boolean leftyFlg;
    public LombokExample(String name) {
        this.name = name;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof LombokExample))
            return false;
        LombokExample other = (LombokExample)o;
        if (!other.canEqual(this))
            return false;
        Object this$name = getName(), other$name = other.getName();
        if ((this$name == null) ? (other$name != null) : !this$name.equals(other$name))
            return false;
        Object this$age = getAge(), other$age = other.getAge();
        if ((this$age == null) ? (other$age != null) : !this$age.equals(other$age))
            return false;
        Object this$leftyFlg = getLeftyFlg(), other$leftyFlg = other.getLeftyFlg();
        return !((this$leftyFlg == null) ? (other$leftyFlg != null) : !this$leftyFlg.equals(other$leftyFlg));
    }
    protected boolean canEqual(Object other) {
        return other instanceof LombokExample;
    }
    public int hashCode() {
        int PRIME = 59;
        result = 1;
        Object $name = getName();
        result = result * 59 + (($name == null) ? 43 : $name.hashCode());
        Object $age = getAge();
        result = result * 59 + (($age == null) ? 43 : $age.hashCode());
        Object $leftyFlg = getLeftyFlg();
        return result * 59 + (($leftyFlg == null) ? 43 : $leftyFlg.hashCode());
    }
    public String toString() {
        return "LombokExample(name=" + getName() + ", age=" + getAge() + ", leftyFlg=" + getLeftyFlg() + ")";
    }
    public String getName() {
        return this.name;
    }
    public Integer getAge() {
        return this.age;
    }
    public Boolean getLeftyFlg() {
        return this.leftyFlg;
    }
}

公式ドキュメント

結局は公式のドキュメントをしっかり読んだほうがためになると思います、本末転倒感ありますが……

@Data
@Getter and @Setter

この辺りのLombokの知識は割とすぐに覚え使いこなし始めると思うのですが、他にも色々なアノテーションやUtilが用意されているので、また別の記事で紹介したいと思います。

それでわ。