内部クラスはエンクロージングインスタンスが無くてもインスタンス化できるのは仕様?

id:backpaper0さんが内部クラスはエンクロージングインスタンスが無くてもインスタンス化できる?と書かれていました。

Inner.class.getConstructor(Outer.class).newInstance((Outer) null);

実行してみたところ、確かにインスタンス化できました。


でも、これってバグなんでしょうか?仕様なんでしょうか?
id:backpaper0さんは「わからない」、これを検証したid:ryoasaiさんは「バグに近い」としています。
そこで、調べてみました。

内部クラスのコンストラク

まず、内部クラスの挙動を確認してみました。
id:ryoasaiさんの検証コード(普通のSI会社では評価されにくいのだけど、多くのシステムは研究熱心な技術者の小さな発見と工夫の積み重ねによって支えられているのでは? - 達人プログラマーを目指して)を改良し、ふつうにエンクロージングインスタンス経由で呼び出すようにします。

public class Outer {

    private String outer = "outer";

    public class Inner {
        public void hello() {
            System.out.println(outer);
        }
    }

    public static void main(String[] args) throws Exception {
        Outer outer = new Outer();
        Inner inner = outer.new Inner();
        inner.hello();
    }
}


これをコンパイルして、クラスファイルを調べてみたら、大体こうなっていました*1

public class Outer$Inner {

    final Outer this$0;

    public Outer$Inner(Outer arg0) {
        this$0 = arg0;
        super();
    }
        
    public void hello() {
        System.out.println(Outer.access$0(this$0)));
    }
}

public class Outer {

    private String outer;

    public Outer() {
        outer = "outer";
    }

    public static void main(String args[]) throws Exception {
        Outer outer = new Outer();
        Outer$Inner inner = new Outer$Inner(outer);
        inner.hello();
    }

    static String access$0(Outer arg0) throws Exception {
        return arg0.outer;
    }
}

ポイントは、Innerクラスのコンストラクタに引数が追加されている点です

この追加された引数は、ヌルチェックなしでメンバ変数に代入されています。そのため、リフレクションで引数に null を渡しても問題なく動いてしまいます。
そのため、id:backpaper0さんが指摘するように「エンクロージングインスタンスがなくても内部クラスを生成できる」ということになります。


ちなみに、hello メソッド内でエンクロージングメソッドを呼び出そうとすると、access$0 内で NullPointerException になります。これが id:ryoasai さんの検証コードで例外が出ると書かれている個所かなと思います。

言語仕様どおりの挙動?

この、「内部クラスがエンクロージングクラスのインスタンス変数を保持する」という挙動は、言語仕様のセクション8.1.3に該当します*2

内部クラスが、それを字句的に見て囲んでいるクラス・メンバであるインスタンス変数を参照している場合、字句的に見て囲んでいるインスタンスに対応した変数が使用される。

8.1.3 内部クラスと囲んでいるインスタンス(Java言語仕様 第三版)


では、肝心の「コンストラクタがこのインスタンス変数に代入する」のは言語仕様のどこに該当するのでしょうか。


Constructor#newInstance(Object...) には、このように書かれています。

コンストラクタの宣言クラスが非 static コンテキスト内の内部クラスである場合、コンストラクタへの最初の引数は囲むインスタンスである必要があります。『Java 言語仕様』のセクション 15.9.3 を参照してください。

Oracle Technology Network for Java Developers | Oracle Technology Network | Oracle


そのセクション15.9.3は・・・。

  • Cが匿名クラスであり、Cの直接のスーパークラスSが内部クラスである場合
    • Sがローカル・クラスであり、Sが静的コンテキスト中に記述されている場合、コンストラクタへと引き渡される引数は、引数リスト中の記述順序に従った引数(あれば)となる。
    • さもなければ、コンストラクタへと引き渡される最初の引数は、i を直接囲んでいる S に関するインスタンスとなり、そのあとにクラス・インスタンス生成式の引数リスト中の記述順序に従った引数(あれば)が続けれられる。
  • さもなければ、コンストラクタへと引き渡される引数は、引数リスト中の記述順序に従った引数(あれば)となる。
15.9.3 コンストラクタとその引数の選択(Java言語仕様 第三版)

あれ?
「Cが内部クラスであり」なら一件落着ですが、「Cが匿名クラスであり」となっています*3
今回調べているのは内部クラスなので、「Cが匿名クラスでない ⇒ コンストラクタへと引き渡される引数は、引数リスト中の記述順序に従った引数となる」となり、挙動と合わなくなってしまいます。


・・・結局、この点だけはよくわかりませんでした(汗)。
(言語仕様が間違っている? 自分がどこかで勘違いしている? 言語仕様の参照すべき個所が違う?)

*1:Javap(逆アセンブル)と Jad(逆コンパイル)の結果から推測しました。

*2:内部クラスは、JDK1.1で追加された仕様です。そのときに、互換性を保ったまま内部クラスを実現しようとしたので、このような遠回しな挙動になったんだと思います。

*3:念のため原文を確認しましたが、そのように書かれています。(If C is an anonymous class, and the direct superclass of C, S, is an inner class, then:)