こんにちは、鈴木です。
TECHSCORE Advent Calendar 2015 の 2 日目の記事です。
今回は Lombok という Java のライブラリをご紹介します。アノテーション一つで単純なアクセッサメソッドを自動生成してくれるなど、何度も書かなければならない「お決まりのコード」を減らしてくれるライブラリです。
この記事のタイトルに含んでいる「Spice up your Java」は Lombok のサイトに書かれていたものです。その言葉通り、ピリリとするスパイスをひと振りするように、普通の Java コードに一味添えてくれます。
それでは以下の順番でお話しします。
お決まりのコード
冒頭で「お決まりのコード」と言いましたが、英語では「ボイラープレートコード(Boilerplate Code)」と呼ぶそうです。具体的には以下のようなコードです。
- 単純に値の取得/設定を行うだけのアクセッサメソッド
- 引数無しのコンストラクタ、final なフィールドの値を全て受け取るコンストラクタ
- toString() をオーバーライドするコード
- equals(), hashCode() をオーバーライドするコード
- 特定の例外を catch して RuntimeException として投げ直すコード
- 例外を投げるだけの private なコンストラクタ(java.lang.Math のように static メソッドを集めたクラスの場合)
- etc..
どれも必要ではあるものの、毎回同じようなコードを書かなければならないことも多く、正直なところ面倒だと感じてしまいます。
使用例: これが、こうなる!
Lombok の機能はいくつもあるのですが、まずは一例をご覧ください。
これが、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
public class User { private String name; private String email; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } @Override public String toString() { ... } @Override public boolean equals(Object object) { ... } @Override public int hashCode() { ... } } |
こうなる!
1 2 3 4 5 |
@Data public class User { private String name; private String email; } |
なんということでしょう。
大量の「お決まりのコード」が @Data というアノテーション一つに置き換えられてしまいました。
お決まりのコードの作り方とタイミング
「Lombok を使うとお決まりのコードを生成してくれるので便利」という趣旨の記事を書いているわけですが、ここで「お決まりのコード」の作り方とタイミングにどのようなパターンがあるのか整理しておきましょう。Lombok が唯一の選択肢ではないということと、他の方法を理解した上で Lombok を評価することが大切だと思うからです。
お決まりのコードの作り方: 自力 or 自動生成
大きく分けると「自分で書く」か「ツール等に自動生成してもらう」のどちらかになります。いくつか具体例を挙げてみます。
- 本当に自力でタイプする
- エディタのコードスニペット機能を使う
- IDE のコード生成機能を使う
- 専用のコードジェネレータを使う
- Lombok を使う
- etc..
他にも色々あると思いますが、次は「お決まりのコード」が作られるタイミングを分類してみます。
お決まりのコードを作るタイミング: コーディング時 or コンパイル時
タイミングは大きく分けると「コーディング時」か「コンパイル時」になります。実行時に生成するという選択肢もありますが、ここでは考えないことにします。(実行時にアクセッサメソッドを作られてもコンパイル時点では参照できないためです。)
コーディング時にコード生成する方法には、IDE のコード生成機能を使う方法などがあります。メリットは生成されたコードを確認しやすいところです。デメリットは変更に弱いところです。例えばフィールド名の変更に伴いアクセッサメソッドを再生成する場合、既存のアクセッサメソッドを手作業で削除しなければなりません。
コンパイル時にコード生成する方法には、一部のコードジェネレータや Lombok があります。メリットは変更にやや強いところで、フィールド名を変更すれば自動的に生成されるアクセッサメソッドの名前も変更されます。デメリットは意図しないコードが生成されていても気付きにくいところです。
どちらが優れているということはないので、やりたいことに合った手段を選びましょう。
Lombok の機能
それでは Lombok の機能をいくつかご紹介します。(詳細は Lombok のサイトをご覧ください。)
val (lombok.val)
val は final な変数を定義するときに使います。
Before:
1 2 |
// 普通のコード final String message = "hello"; |
After:
1 2 |
// Lombok を使ったコード val message = "String"; |
変数の型は右辺の値と同じになります。
もう一度言いますが、変数の型は右辺の値と同じになります。オブジェクト指向の教科書に「変数の型は ArrayList のような具象クラスではなく、List のようなインタフェースにしましょう。」と書かれていることもありますが、その教えを知らぬ間に破ってしまわないように注意しましょう。
以下のコードをみてください。
1 2 |
// Lombok を使ったコード val list = new ArrayList<String>(); |
これは次のコードに変換されます。
1 2 3 4 5 |
// こうなる final ArrayList<String> list = new ArrayList<String>(); // こうはならない final List<String> list = new ArrayList<String>(); |
この場合は、(1) 気にしない、(2) キャストする、(3) val を使わずに普通に書く、のいずれかを選びましょう。
1 2 3 4 5 6 7 8 |
// (1) 気にしない val list = new ArrayList<String>(); // (2) キャストする val list = (List<String>) new ArrayList<String>(); // (3) もしくは val を使わずに普通に書く final List<String> list = new ArrayList<>(); |
もう一点注意ですが、Lombok 1.16.6 では、Lambda 式の中で val を使った場合にコンパイルエラーとなってしまいます。稀ですが Lambda 式の外でもコンパイルエラーになったこともあるので、若干安定性に欠けています。「それでは val は使い物にならないのか?」と問われると、「そこまで悪くはない」というか「うまく動かないケースは避けながらお付き合いすることはできる」という感じです。
@ToString (lombok.ToString)
@EqualsAndHashCode (lombok.EqualsAndHashCode)
@ToString はフィールドの値を含んだ文字列を返す toString() を生成します。@EqualsAndHashCode はフィールドの値を用いて比較を行う equals() とフィールドの値から生成したハッシュ値を返す hashCode() を生成します。
Before:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 普通のコード public class User { private String name; private String email; @Override public String toString() { ... } @Override public boolean equals(Object other) { ... } @Override public int hashCode() { ... } } |
After:
1 2 3 4 5 6 7 |
// Lombok を使ったコード @ToString @EqualsAndHashCode public class User { private String name; private String email; } |
@Value (lombok.Value)
@Data (lombok.Data)
@Value はいわゆる Immutable なクラス(値を変更できないクラス)、@Data は Mutable なクラス(値を変更できるクラス)を定義するときに使用します。使い方はクラスにアノテーションを付けるだけです。
1 2 3 4 5 |
@Data public class User { private String name; private String email; } |
以下に @Value と @Data の違いをまとめます。
@Value が付けられたクラス:
- final なクラスになる(=継承できないクラスになる)
- Getter メソッドが生成される
- toString(), equals(), hashCode() が生成される
- 全フィールドが final になる
- 全フィールドの値を受け取るコンストラクタが生成される
@Data が付けられたクラス
- Getter 及び Setter メソッドが生成される
- toString(), equals(), hashCode() が生成される
- final なフィールドの値を受け取るコンストラクタが生成される
@Getter (lombok.Getter)
@Setter (lombok.Setter)
@Getter は Getter メソッド、@Setter はSetter メソッドを生成します。アノテーションはクラスまたはフィールドに付けることができます。クラスに付けた場合は全てのフィールド、フィールドに付けた場合はそのフィールドのみが対象となります。アクセスレベルを指定することもできます。
使用例(フィールドに @Getter を付ける場合):
1 2 3 4 5 6 |
// フィールドに @Getter を付ける class User { @Getter private String name; private String email; } |
使用例(クラスに @Getter を付ける場合):
1 2 3 4 5 6 |
// クラスに @Getter を付ける @Getter class User { private String name; private String email; } |
使用例(アクセスレベルを指定する場合):
1 2 3 4 5 6 |
// アクセスレベルを指定する class User { @Getter(AccessLevel.PROTECTED) private String name; private String email; } |
@NoArgsConstructor (lombok.NoArgsConstructor)
@RequiredArgsConstructor (lombok.RequiredArgsConstructor)
@AllArgsConstructor (lombok.AllArgsConstructor)
どれもクラスに指定するアノテーションです。@NoArgsConstructor は引数無しコンストラクタ、@RequiredArgsConstructor は final なフィールドの値を受け取るコンストラクタ、@AllArgsConstructor は全フィールドの値を受け取るコンストラクタを生成します。
使用例:
1 2 3 4 5 6 |
// final なフィールドの値 (name) を受け取るコンストラクタを生成する @RequiredArgsConstructor class User { private final String name; private String email; } |
@NonNull (lombok.NonNull)
@NonNull は null チェックを行うコード(値が null なら例外を投げるコード)を生成します。
1 2 3 4 5 6 7 8 9 10 11 12 |
// 引数の name が null なら例外を投げるコードが生成される public void setName(@NonNull String name) { ... } // 以下のコードと同じ public void setName(String name) { if (name == null) { throw new NullPointerException(); } ... } |
@NonNull はフィールドに対して指定することもでき、@Data や @Setter, @AllArgsConstructor などと組み合わせると便利です。以下は @Data と @AllArgsConstructor を組み合わせた例です。
1 2 3 4 5 6 |
@Data @AllArgsConstructor class User { @NonNull private String name; } |
@Data と @AllArgsConstructor によってコンストラクタや Setter メソッドが生成されます。そして、フィールドに @NonNull が付けられている場合、生成されるコンストラクタや Setter メソッドに null チェックのコードが挿入されます。上記のコードは次のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class User { private String name; // @AllArgsConstructor によってコンストラクタが生成される public User(String name) { // @NonNull によって null チェックコードが生成される if (name == null) { throw new NullPointerException(); } this.name = name; } // @Data によって Setter メソッドが生成される public void setName(String name) { // @NonNull によって null チェックコードが生成される if (name == null) { throw new NullPointerException(); } this.name = name; } .... } |
@Cleanup (lombok.Cleanup)
@Cleanup は close() メソッドを呼び出すコードを生成します。
1 |
@Cleanup val reader = new FileReader("a.txt"); |
Java7 以上の場合は try-with-resources 文でも同等のことができるので、どちらを使っても良いでしょう。
1 2 3 |
try (val reader = new FileReader("a.txt")) { ... } |
@SneakyThrows (lombok.SneakyThrows)
@SneakyThrows を使うとチェック例外を非チェック例外であるかのように扱えるようになります。・・・言葉だけの説明だと伝わりにくいと思いますので、まずは @SneakyThrows を用いたコードを見てください。
1 2 3 4 |
@SneakyThrows(InterruptedException.class) public static void sleepOneSecond() { Thread.sleep(1000); } |
Thread.sleep() はチェック例外である InterruptedException を投げるので、普通だと try-catch で囲むか、Thread.sleep() を呼び出しているメソッドの throws に InterruptedException を追加することになります。しかし、@SneakyThrows を使うと上記のように書くことができます。
それでは InterruptedException はどこへ行ってしまうのかというと、どこかへ消えて無くなるわけではありません。Thread.sleep() の中で InterruptedException が発生した場合は、そのまま sleepOneSecond() メソッドの上位階層に伝えられていきます。非チェック例外のように扱われるというだけです。
この「非チェック例外のように」の部分に魔法じみたものを感じると思いますので、その部分を深掘りしてみましょう。まず、上記コードは次のコードに変換されます。
1 2 3 4 5 6 7 |
public static void sleepOneSecond() { try { Thread.sleep(1000); } catch(InterruptedException exception) { Lombok.sneakyThrows(exception); } } |
単純に try-catch で囲まれ、Lombok.sneakyThrows() に catch した例外を渡しています。するとチェック例外が非チェック例外のように生まれ変わって throw される・・。その秘密は Lombok.sneakyThrows() にあるようです。ということで、コードを見ると次のように実装されていました。
1 2 3 4 5 6 7 8 9 10 |
public static RuntimeException sneakyThrow(Throwable t) { if (t == null) throw new NullPointerException("t"); Lombok.<RuntimeException>sneakyThrow0(t); return null; } @SuppressWarnings("unchecked") private static <T extends Throwable> void sneakyThrow0(Throwable t) throws T { throw (T)t; } |
例外は sneakyThrows() から sneakyThrows0() に渡され、その中で throw しているだけでした。
調べてみたところ、チェック例外と非チェック例外の区別はコンパイル時だけのものであり、JVM レイヤー(実行時)では区別されないようです。JVM レイヤーで区別されないというのは、チェック例外だろうと非チェック例外だろうと JVM からすれば「例外を投げる」という同じ処理だということです。それは、コンパイル時のチェックさえすり抜けることができればチェック例外を非チェック例外のように throw することができる、ということです。そして、Generics を使うことでコンパイル時のチェックをすり抜ける、という仕掛けです。
@Log (lombok.extern.java.log.Log)
@Log4j (lombok.extern.log4j.Log4j)
@Log4j2 (lombok.extern.log4j.Log4j2)
@Slf4j (lombok.extern.slf4j.Slf4j)
@XSlf4j (lombok.extern.slf4j.Xslf4j)
@CommonsLog (lombok.extern.apachecommons.CommonsLog)
アプリケーションからログ出力をするときに、以下のような private static final な Logger を定義することは良くあると思います。
1 2 3 4 5 6 7 8 9 10 11 |
import java.util.logging.Logger; public class Main { private static final Logger log = Logger.getLogger(Main.class.getName()); public static void main(String[] args) { log.info("Hello"); } } |
@Log を使うと以下のように書くことができます。上記の「private static final な Logger を定義する」という部分をアノテーションに置き換えることができます。
1 2 3 4 5 6 7 8 9 10 |
import lombok.extern.java.Log; @Log public class Main { public static void main(String[] args) { log.info("Hello"); } } |
@Log 以外にも @Log4j や @Slf4j など、各種 Logger に対応したアノテーションが用意されています。
まとめ
いかがでしたでしょうか。Java を使っていると「手軽にメタプログラミングができるスクリプト言語って楽だなー」と思うことが多いですが、Lombok を使えばそれと似たようなことが実現できます。
Lombok が提供するアノテーションをいくつも紹介しましたが、中には設定ファイルで動作をカスタマイズできるものもあります。例えばアクセッサメソッドの名前に get/set を付けるかどうかを制御することもできます。実験的な機能(まだ正式版ではない機能)の中にも面白そうなものがあります。是非とも Lombok をお試しください。