読者です 読者をやめる 読者になる 読者になる

who’s watching

はじめに

Java Puzzlers Advent Calendar 2016最終日の記事です。
皆さんはこれまでの問題は解けたでしょうか?難しい問題ばかりで僕の正答率はガタガタでしたw

というわけで今日が最後の問題になります。1ヶ月ほど前に記事にしていたのですが、結構面白い内容だったので今回のカレンダー用に再出題してみます。


問題

次のコードの出力は何になるでしょうか?

public class Main {

    public static void main(String[] args) {
        String hello = "hello";

        Runnable a = () -> System.out.println("hello");
        Runnable b = () -> System.out.println(hello);
        Runnable c = new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        };

        WeakReference<Runnable> ref1 = new WeakReference<>(a);
        WeakReference<Runnable> ref2 = new WeakReference<>(b);
        WeakReference<Runnable> ref3 = new WeakReference<>(c);

        a = null;
        b = null;
        c = null;

        System.gc();

        System.out.println(ref1.get() == null);
        System.out.println(ref2.get() == null);
        System.out.println(ref3.get() == null);
    }
}

WeakReferenceを知らない人もいるかもしれないので簡単に解説します。 私達が変数などで使っているのは強参照といいます。WeakReferenceはコンストラクタに与えられたインスタンスが強参照またはソフト参照(今回は使っていない)されなくなった時にGC対象になり、getした時にnullを返すようになります。

それを踏まえた上で今回の問題を考えてみてください!

解説と答え

まず一番わかりやすいref3からみてみましょう。
これは当然GC対象になりnullになります。

次にref1を見てみます。
これの出力はなんとfalseになります。つまりコードにはないどこかからか見ている物があるということです。 ラムダ式はinvokedynamic命令で実行される(必ず使われるというわけではないらしい)のですが、これを使って動的にクラスを作る時作ったインスタンスをキャッシュする仕組みがあります。毎回同じ内容のインスタンスを作るならキャッシュして使い回せばいいよね?って考え方っぽいです。その結果GC対象とならずに出力がfalseになったわけです。

最後にref2を見てみましょう。
これはref1と違って外の変数を参照しているラムダ式です。これではキャッシュして使いまわすことはできません。なのでキャッシュせずに毎回インスタンスを作っています。そのため他の参照がないのでGC対象になって消えます。

というわけで正解は
false
true
true
になります。

詳しい解説は弱参照とラムダ式を御覧ください。

おまけ

この問題は弱参照を使ったObserverパターンを作ろうとしたときに見つけました。
あるコンポーネントのフィールドにListenerを持っておいて、そのインスタンスを弱参照でリスナを登録するとコンポーネントが消えたときに自動的に開放されるじゃん!みたいなノリでした。
Java7までは結構うまく動いていたんですが、Java8が出てラムダ式に書き直したときに見事ハマったわけです。