Java 8 でインスタンスを遅延して構築するいくつかの方法
シングルトンを構築するには、enum を使うのがスレッドセーフだしお手軽です。 なぜなら enum の初期化は1つのスレッドから1度だけ行われると規定されているからです。 また、コンストラクタを private にしてもコンパイルエラーにはならず、何の問題もなく初期化できます。
@ApplicationScoped class EntityManagerProducer { @Produces public EntityManager getEntityManager() { return EntityManagerSingleton.INSTANCE.get(); } private static enum EntityManagerSingleton { INSTANCE; private EntityManager em; private EntityManagerSingleton() { em = Persistence.createEntityManagerFactory("h2-unit").createEntityManager(); } public EntityManager get() { return em; } } }
しかし enum を使う方法だと引数を受け取る事ができません。 つまりシングルトンには使えますが、何らかの値をキャッシュする、最初の一度だけ動くような処理には使えません。
Supplier
注意:冒頭の enum を使うコードはスレッドセーフですが、この章で記述しているコードはスレッドセーフではありません。紛らわしかったのでスレッドセーフにしたバージョンも本記事の末尾に追記しました。
Supplier クラスを使います。 このクラスは Java 8 で追加されたクラスで、ラムダ式を代入する事ができます。
Supplier<Integer> two = () -> 1 + 1; System.out.println(two.get());
以下は初回だけ「1たす1は?」と聞いて2回目以降は「にー」と答える間抜けなクラスです。 (import 等は省略しています)
public class LazyCalc { private static Supplier<Integer> two = () -> init(); public static void main(String[] args) { two.get(); two.get(); two.get(); } private static int init() { System.out.println("1たす1は?"); two = () -> { System.out.println("にー"); return 2; }; return 1 + 1; } }
出力は以下の通りです。
1たす1は? にー にー
…というようなテクニックが Javaによる関数型プログラミング ―Java 8ラムダ式とStream に書いてありました。 他にも平行・並列処理を割と *1 楽に書けたり簡潔に書けるテクニック満載なのでぜひ。
Javaによる関数型プログラミング ―Java 8ラムダ式とStream
- 作者: Venkat Subramaniam,株式会社プログラミングシステム社
- 出版社/メーカー: オライリージャパン
- 発売日: 2014/10/24
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (3件) を見る
他のコード例はこちらをご覧ください。
チェック例外をかいくぐる
今度は一歩進んで、今度は重い処理を疑似的に再現させるため、入力待ちにしてエンターを押すまで実行を止めるようにしてみます。
先ほどの init()
に System.in.read()
を追加して、IOException
を投げるようスロー宣言をメソッドに追加します。
private static int init() throws IOException { System.out.println("1たす1は?"); System.in.read(); two = () -> { System.out.println("にー"); return 2; }; return 1 + 1; }
しかしこれはコンパイルエラーになります。
理由は Supplier#get() に throws IOException が付いていないためです。
上記のチェック例外の制約をかいくぐるために、IOException を RuntimeException にラップする必要があります。
Java 8 にはこのための標準のクラスがあります。UncheckedIOException
クラスです。
public static void main(String[] args) { try { two.get(); two.get(); two.get(); } catch (UncheckedIOException e) { e.getCause().printStackTrace(); } } private static int init() { System.out.println("1たす1は?"); try { System.in.read(); } catch (IOException e) { throw new UncheckedIOException(e); } two = () -> { System.out.println("にー"); return 2; }; return 1 + 1; }
これを実行すると
1たす1は? (エンター) にー にー
無事初回だけ入力待ちにする事ができました!
他のチェック例外に関しては UncheckedIOException のようなラッパークラスは用意されていない事もありますが、自作するのはそう難しくないはずです。
感想
Java のライブラリ自身が最近チェック例外ではなく割と実行時例外を投げるように設計されてるっぽい*2のは、ラムダ式との相性が悪いのもあるんだろうなーと思いました(まる)。 でもチェック例外が全面的に悪いものだとは思えないんだよなー。レイガイムズカシイネ。 結論としては、あるメソッドが投げる例外とスレッドセーフかどうかはなるべく javadoc に書くべき。
あとチェック例外に関しては、代わりにアノテーション、というより Pluggable Annotation Processing API でチェックできるんじゃないか・していくんじゃないかとは思う。 あれは他の言語にはない機能で、C++ 以上にメタ的な情報を、メタな世界ではなく動的なコードで扱える(警告したりコンパイルエラーにしたり)という点で素晴らしいと思ってる。 Java がバージョンを重ねるにつれ、段々とアノテーションだらけになっていくのも無理はない。 Java で Web アプリ書くと設定ファイルだらけと言われてたけど、今は設定ファイル1個も書かずにアノテーションだけで設定を完結させたりもできる。*3
Java はゆっくりだけど、もっともっと変わっていくと思う。
参考リンク
- How thread-safe is enum in java? - Stack Overflow
- 【Effective Java】項目3:private のコンストラクタか enum 型でシングルトン特性を強制する - The King's Museum
- もしもラムダの中で例外が発生したら(後編) - Taste of Tech Topics
- java.io.UncheckedIOException例外クラス - yohhoyの日記
追記(2016/10/8 23:45):「Supplier」の章のコードをスレッドセーフにする
public class LazyCalc { private static Supplier<Integer> two = () -> init(); private static boolean initialized = false; // Future の Thread name 変えるの面倒なので横着して ThreadLocal を使用 private static ThreadLocal<String> threadName = new ThreadLocal<String>(); public static void main(String[] args) { final long start = System.nanoTime(); final ExecutorService executor = Executors.newFixedThreadPool(3); final Future<Integer> a = executor.submit(() -> { threadName.set("a"); return two.get(); }); final Future<Integer> b = executor.submit(() -> { threadName.set("b"); return two.get(); }); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } final Future<Integer> c = executor.submit(() -> { threadName.set("c"); return two.get(); }); executor.shutdown(); try { System.out.println(a.get()); System.out.println(b.get()); System.out.println(c.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } System.out.printf("Program ran in %d seconds.\n", (System.nanoTime() - start) / 1_000_000_000L); } private static synchronized int init() { log("init()"); if (!initialized) { log("1たす1は?"); // 何か重い処理をする try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new UncheckedInterruptedException(e); } two = () -> { log("にー"); return 2; }; initialized = true; } // synchronized の挙動が分かりづらくなる?のでコメントアウトしたけど普通は初期化された後の値を取得するよね… // return two.get(); return 1 + 1; } private static void log(String msg) { System.out.println(threadName.get() + ":" + msg); } static class UncheckedInterruptedException extends RuntimeException { public UncheckedInterruptedException(InterruptedException e) { super(e); } } }
出力結果(スレッド名を付けるようにしてみました)
a:init() a:1たす1は? b:init() c:にー 2 2 2 Program ran in 2 seconds.
コメントにも書いてありますが、上記のコードを説明すると
- a と b がほぼ同時に実行されると期待、2秒スリープした後、c を実行する
- a と b は (A) 経由で
two.get()
を呼び出しているがこれはsynchronized
が付いているので2つともinit()
を実行する事はない - どちらかの実行が終了した後は
initialized == true
となるので if の中の初期化は2つ目のスレッドは実行しない - a と b のスレッドが終了した後に c が実行されると期待される
- ここでは
two.get()
は既に置き換わっているからsynchronized
は付いていない Supplier (B) が直接実行される - よって c 以降は synchronized でロックされる事はない
ぐだぐだ文章で書くより図で説明するとこんな感じ。
- a:two.get()(A)→ synchronized init()(
initialized == false
なので初期化処理が実行される) - b:two.get()(A)→ synchronized init()(
initialized == true
なので初期化処理が実行される) - c:two.get()(B)(two 変数は置き換わった後なのでロック無しの(B)の処理が実行される)