Lombokでコードをスッキリさせる

Java
Java

Javaの冗長なソースコードの記述をアノテーションによって簡略化できる「Lombok」の使い方について書きます。

Lombokとは?

Lombok(ロンボック)は、省略することができない定型的なコード(ボイラープレートコードというらしい)を、アノテーションを使用して省略することができるJavaライブラリです。

ボイラープレートコードの典型的な例として、下記のようなデータを保持するフォームクラスがあります。

public class SampleForm {

    private String name;

    private int age;

    public SampleForm(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

}

名前と年齢の2つのデータしか保持していないのに、コンストラクタやgetter/setterが必要になるせいで、コードの記述量が多くなっています。Eclipseなんかだとgetter/setterを自動補完してくれる機能があるのでまだましですが、すべてタイピングして作るとなると気が引けますね。
また、実際にアプリケーションで使われるようなフォームのデータ数は2つなんかじゃ収まりません。1つのクラスで20~30のデータを保持するなんてこともよくあることです。そうなると、ただのデータ保持クラスが数百行というサイズになるということも。

Lombokを使えば、同じ機能でもコードの記述量はこれだけ変化します。

@Getter
@Setter
@AllArgsConstructor
public class SampleForm {

    private String name;

    private int age;

}

とてもスッキリしましたね!この例ではデータは2つですが、その数が多くなればなるほど、Lombokの実力が発揮されていきます。
せっかくの便利なライブラリなので、使い方を学んでいきましょう!

Lombokの仕組み

あまり詳しくないのでさわりだけ。。。

AST変換という技術が用いられています。AST(Abstract Syntax Tree:抽象構文木)とは、Javaのソースコードをコンパイルするプロセスの中で用いられる、文法構造をツリー形式で表した中間表現です。Javaのソースコードは、このASTを経由してコンパイル(classファイルへの変換)されます。Lombokは、ASTへ変換するときにアノテーションで指定された定型コードを追加した状態のASTにしてくれます。
ソースコードに定型コードを足しこんでからコンパイルしているわけではなく、コンパイルのプロセスに干渉して機能を実現しているので、ソースコードが別に生成されて膨れ上がったりすることなく、軽量に動作します。

Lombokの環境構築

参照設定をして、Lombokが使用可能な環境を整えましょう!
プロジェクト管理ツールはGradleを使用しています。

build.gradle

dependencies {
    // lombok
    compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.18'
}

または

apply plugin: 'war'

dependencies {
    // lombok
    providedCompile group: 'org.projectlombok', name: 'lombok', version: '1.18.18'
}

このようにlombokを依存関係に追加します。
Lombokはコンパイル時に動作し、classファイルができてしまえばお役御免なので、スコープをコンパイルまでとする必要があります。(別に実行時にあっても構わないが、不要なものをそのままにしておかない方がいいです。)
mavenであれば、「<scope>provided</scope>」とすることで実現できます。Gradleでは、これに準ずるものとして「compileOnly」を指定するか、warプラグインを使用したうえで「providedCompile」を使うかといったところが選択肢となります。

# Maven Repositoryでは「providedCompile」が使われているので、推奨されているのはこっちなのかな?

Eclipse等のIDEで、Lombokによって生成されたメソッドをIDEに認識させる(入力候補として表示させるなど)には、IDEにLombokをインストールさせる必要があります。「lombok eclipse」といった感じで検索するとインストールの方法が載ったページが表示されます。なお、私が使っているEclipse pleiades All in One では、インストール段階でLombokもインストール済みなのでこの設定は不要です。

Lombokの使い方

環境が整ったら、さっそく使ってみましょう!

val変数

変数の型名を宣言しなくても、代入された値によってよしなに型推論してくれます。finalとして定義されるので再代入はできません。

public static void main(String[] args) {
    val unknown = new HashMap<String, List>();
    System.out.println(unknown.getClass());    // class java.util.HashMap
}
型の情報は冗長だとは思わない(ちゃんと書いたほうが可読性は高いと思っている)ので、これは使わないかな。

@NonNull

public static void main(String[] args) {
    sampleMethod("hello");   // hello
    sampleMethod(null);      // Exception in thread "main" java.lang.NullPointerException:
                             // str is marked non-null but is null
}

private static void sampleMethod(@NonNull String str) {
    System.out.println(str);
}

引数に@NonNullアノテーションを指定することで、引数のNullチェックを自動生成してくれます。

@Cleanup

private static void sampleMethod(String path) throws IOException {
    @Cleanup BufferedReader in = new BufferedReader(new FileReader(path));
    System.out.println(in.readLine());
}

メソッドの終わりにclose()メソッドを呼び出してリソースをクリーンアップします。

    @Cleanup("close") BufferedReader in = new BufferedReader(new FileReader(path));

このように書くことで、クローズのために実行するメソッドを明示的に指定することもできます。
同じ動作を、Java7から導入されたtry-with-resources構文で書くとこうなります。

private static void sampleMethod(String path) throws IOException {
    try (BufferedReader in = new BufferedReader(new FileReader(path))) {
        System.out.println(in.readLine());
    } catch (IOException ioe) {
        throw ioe;
    }
}

この構文が使用できる前までの、finally句で非null判定してcloseを呼び出すようなコードは本当に冗長でしたが、このtry-with-resources構文は非常にスッキリしています。catch句をちゃんと書くためにも、こちらを使用するのが無難かと思います。

@Getter / @Setter

@Getter
@Setter
public class SampleForm {
    private String name;
    private int age;
}

メンバ変数のGetterとSetterを自動的に生成してくれます。

@Getter(AccessLevel.PRIVATE)
private String name;

クラス単位ではなく、メンバ変数単位でアノテーションを付与することができます。
また、AccessLevelを指定することで、GetterまたはSetterの可視性を指定できます。

@ToString

@Getter
@Setter
@ToString(exclude = "secret")
public class SampleForm {
    private String name;
    private int age;
    private List hobbys;
    private String secret;
}

ObjectクラスのtoStringメソッドのオーバーライドを自動的に生成してくれます。
executeで出力しないメンバ変数を指定できます。
フォーマットはこのようになっています。

public static void main(String[] args) {
    SampleForm form = new SampleForm();
    form.setName("inashun");
    form.setAge(24);
    form.setHobbys(Arrays.asList("ドライブ", "カメラ"));
    form.setSecret("最近ちょっと太りました");
    System.out.println(form.toString());
}
SampleForm(name=inashun, age=24, hobbys=[ドライブ, カメラ])

@EqualsAndHashCode

@EqualsAndHashCode
public class SampleForm {
    private String name;
    private int age;
}

ObjectクラスのequalsメソッドとHashCodeメソッドのオーバーライドを自動的に生成してくれます。
equalsメソッドは、インスタンスのすべてのメンバ変数の内容がそれぞれ一致しているかを比較します。

public static void main(String[] args) {
    SampleForm form1 = new SampleForm();
    form1.setName("inashun");
    form1.setAge(24);

    SampleForm form2 = new SampleForm();
    form2.setName("inashun");
    form2.setAge(24);

    System.out.println("HashCode:form1 - " + form1.hashCode());
    System.out.println("HashCode:form2 - " + form2.hashCode());
    System.out.println(form1.equals(form2));
}
HashCode:form1 - 1940928683
HashCode:form2 - 1940928683
true

変数の内容が一致すればequalsはtrueとなります。

    form1.setAge(24);
    form2.setAge(25);
HashCode:form1 - 1940928683
HashCode:form2 - 1940928742
false

変数の内容が一致しなければfalseとなります。

@NoArgsConstructor

@NoArgsConstructor
public class SampleForm {
    private final String name;
    private int age;
}

引数なしのコンストラクタを自動的に生成してくれます。

@RequiredArgsConstructor

@RequiredArgsConstructor
public class SampleForm {
    private final String name;
    private int age;
}

final修飾子のついたメンバ変数のみを引数に取るコンストラクタを自動的に生成してくれます。

@AllArgsConstructor

@AllArgsConstructor
public class SampleForm {
    private final String name;
    private int age;
}

すべてのメンバ変数を引数に取るコンストラクタを自動的に生成してくれます。

また、ここまでの3つのコンストラクタを自動生成するアノテーションでは、staticName属性を指定することで、ファクトリメソッドを自動生成することができます。

@AllArgsConstructor(staticName = "create")
public class SampleForm {
    private final String name;
    private int age;
}

コンストラクタがprivateになり、代わりにstaticなファクトリメソッドが作成されています。これで外部クラスでnewによるインスタンス生成をさせないようにすることができます。

@Data

@Data
public class SampleForm {
    private final String name;
    private int age;
}

下記のアノテーションすべてを付与したのと同様の状態となります。

  • @ToString
  • @EqualsAndHashCode
  • @Getter
  • @Setter
  • @RequiredArgsConstructor

@Value

@Value
public class SampleForm {
    String name;
    int age;
}

下記のアノテーションすべてを付与したのと同様の状態となります。

  • @ToString
  • @EqualsAndHashCode
  • @Getter
  • @AllArgsConstructor

また、クラスおよびメンバ変数はすべてfinalとなり、メンバ変数は可視性を指定していなくても自動的にprivateになります。
一度インスタンスを生成したら変更することができない、イミュータブルな実装を強制するために使用することができます。

また、@Dataと@Valueでは、staticConstructor属性を指定することで、ファクトリメソッドを自動生成することができます。

@Value(staticConstructor = "of")
public class SampleForm {
    String name;
    int age;
}

@Builder

@Value
@Builder
public class SampleForm {
    String name;
    int age;
}
public static void main(String[] args) {
    SampleFormBuilder builder = SampleForm.builder()
                                          .name("inashun")
                                          .age(24);
    SampleForm form = builder.build();
    System.out.println(form);  // SampleForm(name=inashun, age=24)
}

ビルダークラスを自動的に生成してくれます。@Valueをつけるとインスタンスの生成の段階ですべてのデータを格納しなければならないので、コンストラクタ(またはファクトリメソッド)だとデータの数が増えたときにコードが煩雑になったり、分岐によって中間変数が大量発生したりします。そんなときにはビルダークラスを作成するようにしましょう。
# ちなみに、この時のコンストラクタはパッケージプライベートとなります。

@Singular

@Value
@Builder
public class SampleForm {
    String name;
    int age;
    @Singular
    List skills;
}
public static void main(String[] args) {
    SampleForm form = SampleForm.builder()
                                .name("inashun")
                                .age(24)
                                .skill("java")
                                .skill("PostgreSQL")
                                .skills(Arrays.asList("AWS", "Git"))
                                .build();
    System.out.println(form);  // SampleForm(name=inashun, age=24, skills=[java, PostgreSQL, AWS, Git])
}

ビルダークラスにおいて、コレクション型のメンバ変数のセッターメソッドを上書きではなく追加として作成したい場合に、対象のコレクション型のメンバ変数に@Singularをつけます。

対象のコレクション型のメンバ変数名を「~s」というように複数形の単語にしておくと、単一の値をとるセッターメソッドは単数形、コレクションをとるセッターメソッドは複数形というように、メソッド名が変化してより可読性が上がります。公式ドキュメントを見ると、単数形-複数形の変換以外のパターン(~List など)はサポートされていないようでした。@Singularの対象となる変数は複数形「~s」とするようにしましょう。

まとめ

コードがスッキリして書くのが楽になるな、というのが当初のLombokの印象でしたが、調べていくうちに、デザインパターンまで意識したモダンで良質なコーディングに導いてくれるという別の要素に気づかせて貰いました。どんどん活用していきましょう!

参考

Lombok 使い方メモ – Qiita
Lombok – Qiita
Mave Repository
Project Lombok

コメント

タイトルとURLをコピーしました