Works by ...

プログラミング関連でメモする

【Java】 リフレクションで取得したインナークラスコンストラクタの引数は、エンクロージングクラスのインスタンスを必要とする

背景

単体テストでインナークラスのテストをしたい(テストしやすい設計にすべきかどうかは別として)ときに、インナークラスのコンストラクタがprivateの場合、インスタンス化をリフレクションで行いたい。。。

・このとき、getDeclaredConstructor()で取得したコンストラクタオブジェクトの引数型が想定と異なり小一時間詰まってしまった

具体例

例えば以下のようなクラス定義があるとする

public class Outer {

    public class Inner{

        public Inner(){
            // 引数なしコンストラクタ
        }

        public Inner(String str){
            // String形変数を一つ引数に取るコンストラクタ
        }

    }

}

エンクロージングクラスとしてOuterクラスを、 インナークラスとしてInnerクラスを定義しました。

次に単体テストとして、以下コードを記述

public class TestInner {

    @Test
    public void test() throws Exception{

        // コンストラクタ一覧
        Constructor<?>[] constructors = Outer.Inner.class
                .getDeclaredConstructors();

        // 全コンストラクタのコンストラクタタイプを取得
        Arrays.asList(constructors).forEach(
                c -> {
                    System.out.println(c.getName() + ":");
                    Arrays.asList(c.getParameterTypes()).forEach(p -> System
                            .out.println(p));
                    System.out.println();
                }
        );

        // NG
        // constructors[0].newInstance();

        // OK
        constructors[0].newInstance(new Outer());

    }

}

上記テストを実行

Outer$Inner:
class Outer

Outer$Inner:
class Outer
class java.lang.String

出力を見ると、コンストラクタの引数型に親クラス型が含まれているのがわかります。 実際にインスタンス化するときにも、親クラスのインスタンスをコンストラクタ引数として渡す必要があることがわかります。

なぜ親クラスインスタンスが必要?

親クラスインスタンスが必要な理由は、インナークラスであるInnerにstatic修飾子をつけると理解できます。

public class Outer {

    public static class Inner{

        public Inner(){
            // 引数なしコンストラクタ
        }

        public Inner(String str){
            // String形変数を一つ引数に取るコンストラクタ
        }

    }

}

上記クラス定義でテストを実行してみると、各コンストラクタがOuterインスタンス を引数に取らなくなります。

public class TestInner {

    @Test
    public void test() throws Exception{

        // コンストラクタ一覧
        Constructor<?>[] constructors = Outer.Inner.class
                .getDeclaredConstructors();

        // 全コンストラクタのコンストラクタタイプを取得
        Arrays.asList(constructors).forEach(
                c -> {
                    System.out.println(c.getName() + ":");
                    Arrays.asList(c.getParameterTypes()).forEach(p -> System
                            .out.println(p));
                    System.out.println();
                }
        );

        // InnerがstaticだとOK
        constructors[0].newInstance();

        // InnerがstaticだとNG
        // constructors[0].newInstance(new Outer());

    }

}
Outer$Inner:

Outer$Inner:
class java.lang.String

インナークラスのクラス定義は非staticであり、エンクロージングクラスがインスタンス化されて初めてクラス定義が参照可能になります。 そのため、リフレクションで取得したコンストラクタは、非staticなインナークラスをインスタンス化するためにエンクロージングクラスインスタンスを取得しているのだと考えられます。

// Innerが非staticの場合
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();

// Innerがstaticの場合
Outer.Inner inner = new Outer.Inner();

おわりに

どのクラスからも参照可能であるがインスタンス化は特定のクラスでのみやりたい、みたいなシチュエーションは割りとあったので、これからもこのやり方を使っていく気がする