リフレクションでのローカルクラスのコンストラクタ引数

ローカルクラスは定義されたメソッド*1のローカル変数を参照することができる。
バイトコード上どうなってるの?と知りたかったのでリフレクションで簡単に調査。

まとめ

ローカルクラス*2のコンストラクタ引数の型は、リフレクションで見ると

  1. (non-static contextで宣言されている場合のみ)Enclosingインスタンスの型
  2. (ある場合のみ)コンストラクタ引数の型(複数)
  3. (ある場合のみ)参照されているローカル変数の型(複数)

になる。

サンプル

package test;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

class Enclosing {
    public Class<?> getLocalClass() {
        // finalローカル変数
        final RefFromConstructor refFromConstructor = new RefFromConstructor();
        final RefFromInitializer refFromInitializer = new RefFromInitializer();
        final RefFromField refFromField = new RefFromField();
        final RefFromMethod refFromMethod = new RefFromMethod();
        // 参照されていない
        final NoRef noRef = new NoRef();
        
        // ローカルクラス
        class Local {
            // コンストラクタ 引数
            public Local(final Arg1 arg1, final Arg2 arg2) {
                // コンストラクタからローカル変数を参照
                noAction(refFromConstructor);
            }

            {
                // インスタンス初期化子からローカル変数を参照
                noAction(refFromInitializer);
            }
            
            // フィールド初期化からローカル変数を参照
            private Void field = noAction(refFromField);
            
            public void method() {
                // メソッドからローカル変数を参照
                noAction(refFromMethod);
                System.out.println("method");
            }

            
            private Void noAction(final Object o) {
                // 何もしない
                return null;
            }

        }
        // ローカルクラスを返す
        return Local.class;
    }
}

class Arg1 {/**/}
class Arg2 {/**/}
class RefFromConstructor {/**/}
class RefFromInitializer {/**/}
class RefFromField {/**/}
class RefFromMethod {/**/}
class NoRef {/**/}

public class LocalClassTest {

    public static void main(final String[] args) throws NoSuchMethodException,
            SecurityException, IllegalAccessException,
            IllegalArgumentException, InvocationTargetException,
            InstantiationException {
        Class<?> clazz = new Enclosing().getLocalClass();
        for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
            // コントストラクタを表示
            System.out.println(constructor);
        }
        Constructor<?> constructor = clazz.getConstructor(Enclosing.class,
                Arg1.class,
                Arg2.class,
                RefFromInitializer.class,
                RefFromField.class,
                RefFromConstructor.class,
                RefFromMethod.class);
        Object local = constructor.newInstance(new Enclosing(),
                new Arg1(),
                new Arg2(),
                new RefFromInitializer(),
                new RefFromField(),
                new RefFromConstructor(),
                new RefFromMethod());
        Method method = clazz.getMethod("method");
        method.invoke(local);
    }

}

実行結果

public test.Enclosing$1Local(test.Enclosing,test.Arg1,test.Arg2,test.RefFromInitializer,test.RefFromField,test.RefFromConstructor,test.RefFromMethod)
method

コンストラクタの引数の型にEnclosing, Arg, Ref すべてが現れていることがわかる。
ローカル変数はコンストラクタ引数として渡されていたということだ。
なお、インスタンス生成もメソッド呼び出しもリフレクションで外側から問題なくできる。
あんまりローカルじゃない。

補足

コンストラクタ引数に現れる、参照されているローカル変数の順序は、ローカルクラスのどこから参照されているか、と出現順で決まるようだ。
ソースコード上でローカルクラス内のコンストラクタ、インスタンス初期化子、フィールド宣言、メソッドの順番を入れ替えて調べたところ

  1. 優先順位はフィールド初期化 = インスタンス初期化子 > コンストラクタ = メソッド の順
  2. 優先順位が同じならばソースコードの出現順

のようだ。
予測しづらいので、リフレクションでコンストラクタ呼び出しは素人にはおすすめできない

関連

(2013/01/13追加)
内部クラスのコンストラクタ - しげるメモにコンストラクタ変換の分かりやすい解説があることを知りました。
無名クラスはスーパークラスのコンストラクタを必ず呼び出すので元のコンストラクタが特定できると。なるほど。

*1:コンストラクタや初期化子かも

*2:無名クラスも同様