Humanity

Edit the world by your favorite way

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

Javaによる関数型プログラミング ―Java 8ラムダ式とStream

他のコード例はこちらをご覧ください。

github.com

チェック例外をかいくぐる

今度は一歩進んで、今度は重い処理を疑似的に再現させるため、入力待ちにしてエンターを押すまで実行を止めるようにしてみます。 先ほどの 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 はゆっくりだけど、もっともっと変わっていくと思う。

参考リンク

追記(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)の処理が実行される)

*1:まぁ正直言うと、個人的には平行・並列処理書くならメッセージパッシングできる言語機能なりライブラリがある言語(Go、ScalaErlang 等々)の方がもっと楽に安全に書けるんだろうな…というのはあるけど

*2:JAX-RSアノテーションベースでなるべく throws を書かないとか。最近は throws Exception とかいうかっこ悪い API を実装させるインターフェースは少なくなってきてるように思います

*3:本格的に最近の Java EE 調べ始めたのが最近なので「今更?」と言うのはご容赦ください…