Edited at

【Java】lombokの@Getter(lazy=true)でKotlinのlazy byのように遅延初期化する


遅延初期化とは

「初期化されていない場合初期化し、初期化されていた場合はその値を用いる」的な挙動を実現することです。

これによって、重い計算など時間のかかる処理を必要とする初期化を「必要に応じて一度のみ」行うことが実現できます。

ただし、素朴に書いた場合、単純にコード量が増えるだけでなく、変数がどこで初期化されるか意識する必要があるなど面倒があります。


Kotlinでの遅延初期化

Kotlinではby lazyで遅延初期化できる機能が備わっています。

公式ドキュメントより、以下のように宣言した変数は遅延初期化されます。

また、初期化されるのは一度のみであるため、ラムダ内に呼び出すたび値が変わるような処理(e.g. 乱数生成)を書いたとしても、その値は初期化後に変化しません。



val lazyValue: String by lazy {

println("computed!")
"Hello"
}


lombokを用いた遅延初期化

標準のJavaでは遅延初期化を実現する機能は提供されていませんが、lombokの黒魔術パワーを使えば簡単に書けます。

@Getter(lazy=true)に対してメソッド呼び出しを渡すことで遅延初期化できます。

ここで、@Getter(lazy=true)したフィールドはゲッターを介してのみアクセス可能である(= そのフィールド名では呼び出せない)ことに注意が必要です。

@Getter(lazy = true) private final double[] cached = ((Supplier<double[]>) () -> {

double[] result = new double[1000000];
for (int i = 0; i < result.length; i++) {
result[i] = Math.asin(i);
}
return result;
}).get();

一応他の関数型インターフェースでも同等の動きが実現できますが、今回はKotlin風を意識したためSupplierを用いています。


補足1

今回はSupplierをその場で初期化 & 呼び出す形にしましたが、Supplierを別に変数として取っておき、遅延初期化でそれを呼び出す(= 状況に応じて初期化処理を書き換える)ような書き方も可能です。

private final Supplier<double[]>() supplier; // このフィールドをコンストラクタなどで初期化

@Getter(lazy = true) private final double[] cached = supplier.get();


補足2

公式ドキュメントではメソッドとフィールドを分ける形で書かれています。

個人的には、初期化用に使い捨てられる感が出ていた方が良いと感じるので、Java8を使える環境であればSupplierを宣言即呼び出しで渡す方がよいと思います。



@Getter(lazy=true) private final double[] cached = expensive();

private double[] expensive() {
double[] result = new double[1000000];
for (int i = 0; i < result.length; i++) {
result[i] = Math.asin(i);
}
return result;
}


結論

Javaで書いてもダサいのでKotlinで書いた方が良いと思います。