エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

ラムダ式のオブジェクトは再利用される場合がある

Java8で導入されたlambda。このlambdaのオブジェクトがどの単位でインスタンス化されるかが気になったので試してみた。
というのも、lambdaのオブジェクト生成はJavaVM側の制御に任されているため、どのような単位でオブジェクトが生成されるのか純粋に興味があったのと、このあたりの話はInvokeDynamicの採用理由とも関連がありそうに思えたからだ。

そこで、まず以下のようなコードを書いて試してみた。

public class LambdaTest {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            test();
        }
    }
    private static void test() {
        Runnable runnable = () -> {};
        System.out.println("lambda: "
                + System.identityHashCode(runnable));
    }
}

実行結果は以下の通り。この場合はlambda記法ではオブジェクトのハッシュ値が同じになっている。つまり、この場合はrunnableオブジェクトが1つ生成された後は、使いまわされている事がわかる。

lambda: 1205044462
lambda: 1205044462
lambda: 1205044462

では、明らかにオブジェクトを使いまわすことが出来ないケースを考えてみる。以下の例ではtestメソッドの引数「x」は事実上のfinalであり、このxをlambdaの処理内で参照している。この場合、匿名クラスの場合もそうだが、runnableオブジェクト側に「x」に相当するfinalフィールドが暗黙的に作成され、そこに値が保持されるため、オブジェクトを再利用することは出来ないはずだ。

public class LambdaArgTest {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            test(123);
        }
    }
    private static void test(int x) {
        Runnable runnable = () -> System.out.println(x);
        System.out.println("lambda(" + x + "): "
                + System.identityHashCode(runnable));
    }
}

実行結果は以下の通り。やはり、この場合は毎回異なるオブジェクトが生成された。また、この例では毎回「123」という同一の値を引数に与えているが、そうであってもオブジェクトの再利用はされないようだ。

lambda(123): 168423058
lambda(123): 821270929
lambda(123): 1160460865

ここでデバッガーの力を借りて、runnableオブジェクトを確認してみた。期待通り、runnableオブジェクトにはarg$1というフィールドが暗黙的に作成されており、その値として、引数で与えた「123」という値が保持されていることが確認できた。

念のため、ソースコードを少し変更してリフレクションを用いてlambdaオブジェクトの持っているフィールドを一覧してみる。

public class LambdaArgTest {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            test(123);
        }
    }
    private static void test(int x) {
        Runnable runnable = () -> System.out.println(x);
        System.out.println("lambda(" + x + "): "
                + System.identityHashCode(runnable));
        System.out.println(Arrays.toString(
                runnable.getClass().getDeclaredFields()));
    }
}

実行結果はこちら。private final int の arg$1 というフィールドが自動生成されていることが分かる。

lambda(123): 168423058
[private final int test0.LambdaArgTest$$Lambda$1/1555009629.arg$1]
lambda(123): 821270929
[private final int test0.LambdaArgTest$$Lambda$1/1555009629.arg$1]
lambda(123): 1160460865
[private final int test0.LambdaArgTest$$Lambda$1/1555009629.arg$1]

以上のことから、lambdaに対応するオブジェクトが再利用可能なオブジェクトであると見なせる場合はオブジェクトは再利用され、再利用出来ないような状況である場合にはオブジェクトが毎回作られるといった賢い挙動をしていることが確認できた。このような挙動が可能になるのは、ひとえにlambdaに対応するオブジェクトの生成がJavaVM側に任されているためだ。

これが、lambdaの代わりに匿名クラスで記述した場合はどうなるか。通常、ソースコードは以下のようになり、明示的な「new」を毎回記述するようなスタイルで記述することになる。つまり、これではrunnableオブジェクトが再利用可能か否かをJavaVM側で判断するような余地が残されておらず、JavaVMとしては「new」の指示に従って異なるオブジェクトを生成せざるを得ない。

public class AnonTest {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            test();
        }
    }
    private static void test() {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {}
        };
        System.out.println("anon class: "
                + System.identityHashCode(runnable));
    }
}

その結果、以下の実行結果のように、毎回異なるオブジェクトが生成される。

anon class: 356573597
anon class: 1735600054
anon class: 21685669

[近棟 稔]