Java のインナークラスにおける裏舞台の全貌

今日 Java のインナークラスとリフレクション関連でいくつか発見したことがあったので、ここに残しておきます。

以下のようなコードがあります。

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

public class Foo {
  private class Bar {
    private Bar() {
    }
    private void baz() {
      System.out.println("zap! zap! zap!");
    }
  }
  private void foo() throws Exception {
    new Bar().baz(); // (5)
    Class clazz = Class.forName("Foo$Bar"); // (1)
    Constructor constructor = clazz.getDeclaredConstructor(new Class[] { Foo.class}); // (2)
    constructor.setAccessible(true); // (3)
    Method method = clazz.getDeclaredMethod("baz", null); // (4)
    method.setAccessible(true);
    Object obj = constructor.newInstance(new Object[] { this });
    method.invoke(obj, null);
    Constructor[] constructors = clazz.getDeclaredConstructors(); 
    for (int i = 0; i < constructors.length; i++) {
      System.out.println("constructors[" + i + "]:" + constructors[i]); // (5)
    }
    Field[] fields = clazz.getDeclaredFields();
    for (int i = 0; i < fields.length; i++) {
      System.out.println("fields[" + i + "]:" + fields[i]); // (2)
    }
  }
  public static void main(String[] args) throws Exception {
    new Foo().foo();
  }
}

単に Foo 内の Bar クラスの baz() メソッドにリフレクションでアクセスしているだけですが、順を追って解説します。

(1) インナークラス名

インナークラスのクラス名指定は $ が使われます。意外と忘れっぽいので注意しましょう。

(2) コンストラクタへの暗黙の引数

ここが本日の発見のひとつ目。

Bar のコンストラクタはプライベートなので Class#getConstructor() では取得できず、Class#getDeclaredConstructor() を使用して取得します。
これは基本なのでよいでしょう。

再び Bar のコンストラクタを見ると、引数がひとつもありません。
しかし、ここでは Foo.class を引数に取るコンストラクタを取得しています。

インナークラスの場合、コンパイル時にそのアウタークラスが暗黙で第一引数で渡されるようにコンストラクタが書き換えられます。
そして、this$0 という final なアウタークラスのフィールドが生成され、そこに参照が保持されます。

ちなみに Java の場合、あまり知られていないようですが $ は変数名として使用することが可能です。Java 言語仕様として以下に記載されています。

The Java letters include uppercase and lowercase ASCII Latin letters A-Z (\u0041-\u005a), and a-z (\u0061-\u007a), and, for historical reasons, the ASCII underscore (_, or \u005f) and dollar sign ($, or \u0024).

意訳:Java 文字には、'A-Z' と 'a-z'、また歴史的な理由によって、'_' と '$' が含まれる。

3.8 Identifiers

つまり、以下のようなコードは正しい Java コードです。

   int $ = 100;
   int $$ = 200;
   int $$$ = $ + $$;

ただし、インナークラスにおいてのみ this$0 というフィールド名で変数を定義することはできません。
自動生成されるフィールドとかぶってしまうためです。
コンパイル時に "Duplicate field Foo.Bar.this$0" というメッセージが出力されます。

変数名の $ は、Java 言語仕様で使用することが許容されているにもかかわらず、通常は以下のように使用することを自粛するように要求されています。

The $ character should be used only in mechanically generated source code or, rarely, to access preexisting names on legacy systems.

意訳:'$' は自動生成されたソースコードか、レガシーシステムにアクセスする場合のみ使用するのが望ましい。

3.8 Identifiers

話が少しそれますが、JSPEJB が自動生成するコードは、先頭にアンダースコアが使用されることが多く、javac や RMI の自動生成するコードは $ が使用されることを確認しています。
先頭アンダースコアも自動生成されるコードが使用することがあるため、プログラマは使用しない方が良い、というのが私の持論です。

(3) private へのアクセス許可

setAccessible(true) を呼び出すことで、private にアクセスすることができるようになります。ただし、セキュリティマネージャで許可されている場合に限ります。

(4) メソッド取得

メソッドも、private なものを取得する場合は getMethod() ではなく getDeclaredMethod() を使用します。

(5) 定義されているコンストラク

ここが本日の発見のふたつ目。

ここではすべてのコンストラクタを出力しています。

このプログラムの出力を見てみましょう。

zap! zap! zap!
zap! zap! zap!
constructors[0]:private Foo$Bar(Foo)     ← ここと
constructors[1]:Foo$Bar(Foo,Foo$Bar)     ← ここ
fields[0]:final Foo Foo$Bar.this$0

コンストラクタはひとつしか定義していないはずなのに、なぜかふたつ見つかりました。

見つかったひとつ目は、ソースコード上に定義した引数を取らないコンストラクタにアウタークラスを暗黙に渡すものなので問題ありません。

問題はふたつ目です。
これは第二引数に自分自身を受け取るようになっています。
そんなコンストラクタはどこにも定義していません。

実はこれ、foo() メソッドの一番最初のコード「new Bar().baz();」があるかないかで存在するかしないかが変わります。
アウタークラスのコンテキストでインナークラスが生成された場合、自動生成されるのです。
バイトコードを解析したところ、第一引数の Foo を使用してオリジナルのコンストラクタを呼び出しているだけでした。

これは、インナークラスのコンストラクタが private で定義されているのですが、言語仕様上アウタークラスはインナークラスの private コンストラクタにアクセスすることができ、しかし Java クラス仕様として private は private であって外部からはアクセスできず、したがってこのようなシグネチャの異なるコンストラクタを自動生成して、そちらから private コンストラクタにアクセスするようになっています。

では自動生成される予定のコンストラクタ Bar(Bar) を定義しておいたらどうなるでしょうか。

自動生成されるコンストラクタは、この場合 Bar(Foo, Bar, Bar) のようになり、うまく回避されるようになっています。

ここで気になるのが、「じゃあメソッドだって private じゃん」ということです。

メソッドはメソッド毎に access$1 などという名前のブリッジメソッドが自動生成されるようになっています。
いやいや、インナークラスの裏舞台はこういう風になっていたんですね。
よくできています。

おまけ

javap でバイトコードを解析した結果、インナークラスを含むアウタークラスには class$0 という変数が自動生成されるようになっています。

インナークラスのクラス名に $ が含まれるのも、その内部に this$0 というフィールドを持つのも、access$1 というメソッドを持つのも、アウタークラスに class$0 というフィールドを持つのも、匿名クラスが $0 から始まるのも、java 1.1 から導入されたインナークラスが始まりです。
もしかして歴史的な経緯というのは、1.1 でインナークラスを導入する際に $ を使用できるようにしたこと*1を指しているのかもしれません。

なお、以下に javap の結果を貼っておきます。
興味のある方は眺めてみてください。

$ javap -c -private Foo
Compiled from "Foo.java"
public class Foo extends java.lang.Object{
static java.lang.Class class$0;

public Foo();
  Code:
   0:   aload_0
   1:   invokespecial   #11; //Method java/lang/Object."<init>":()V
   4:   return

private void foo()   throws java.lang.Exception;
  Code:
   0:   new     #21; //class Foo$Bar
   3:   dup
   4:   aload_0
   5:   aconst_null
   6:   invokespecial   #23; //Method Foo$Bar."<init>":(LFoo;LFoo$Bar;)V
   9:   invokestatic    #26; //Method Foo$Bar.access$1:(LFoo$Bar;)V
   12:  ldc     #30; //String Foo$Bar
   14:  invokestatic    #31; //Method java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class;
   17:  astore_1
   18:  aload_1
   19:  iconst_1
   20:  anewarray       #32; //class java/lang/Class
   23:  dup
   24:  iconst_0
   25:  getstatic       #37; //Field class$0:Ljava/lang/Class;
   28:  dup
   29:  ifnonnull       57
   32:  pop
   33:  ldc     #39; //String Foo
   35:  invokestatic    #31; //Method java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class;
   38:  dup
   39:  putstatic       #37; //Field class$0:Ljava/lang/Class;
   42:  goto    57
   45:  new     #40; //class java/lang/NoClassDefFoundError
   48:  dup_x1
   49:  swap
   50:  invokevirtual   #42; //Method java/lang/Throwable.getMessage:()Ljava/lang/String;
   53:  invokespecial   #48; //Method java/lang/NoClassDefFoundError."<init>":(Ljava/lang/String;)V
   56:  athrow
   57:  aastore
   58:  invokevirtual   #51; //Method java/lang/Class.getDeclaredConstructor:([Ljava/lang/Class;)Ljava/lang/reflect/Constructor;
   61:  astore_2
   62:  aload_2
   63:  iconst_1
   64:  invokevirtual   #55; //Method java/lang/reflect/Constructor.setAccessible:(Z)V
   67:  aload_1
   68:  ldc     #61; //String baz
   70:  aconst_null
   71:  invokevirtual   #63; //Method java/lang/Class.getDeclaredMethod:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
   74:  astore_3
   75:  aload_3
   76:  iconst_1
   77:  invokevirtual   #67; //Method java/lang/reflect/Method.setAccessible:(Z)V
   80:  aload_2
   81:  iconst_1
   82:  anewarray       #3; //class java/lang/Object
   85:  dup
   86:  iconst_0
   87:  aload_0
   88:  aastore
   89:  invokevirtual   #70; //Method java/lang/reflect/Constructor.newInstance:([Ljava/lang/Object;)Ljava/lang/Object;
   92:  astore  4
   94:  aload_3
   95:  aload   4
   97:  aconst_null
   98:  invokevirtual   #74; //Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
   101: pop
   102: aload_1
   103: invokevirtual   #78; //Method java/lang/Class.getDeclaredConstructors:()[Ljava/lang/reflect/Constructor;
   106: astore  5
   108: iconst_0
   109: istore  6
   111: goto    153
   114: getstatic       #82; //Field java/lang/System.out:Ljava/io/PrintStream;
   117: new     #88; //class java/lang/StringBuffer
   120: dup
   121: ldc     #90; //String constructors[
   123: invokespecial   #92; //Method java/lang/StringBuffer."<init>":(Ljava/lang/String;)V
   126: iload   6
   128: invokevirtual   #93; //Method java/lang/StringBuffer.append:(I)Ljava/lang/StringBuffer;
   131: ldc     #97; //String ]:
   133: invokevirtual   #99; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
   136: aload   5
   138: iload   6
   140: aaload
   141: invokevirtual   #102; //Method java/lang/StringBuffer.append:(Ljava/lang/Object;)Ljava/lang/StringBuffer;
   144: invokevirtual   #105; //Method java/lang/StringBuffer.toString:()Ljava/lang/String;
   147: invokevirtual   #108; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   150: iinc    6, 1
   153: iload   6
   155: aload   5
   157: arraylength
   158: if_icmplt       114
   161: aload_1
   162: invokevirtual   #113; //Method java/lang/Class.getDeclaredFields:()[Ljava/lang/reflect/Field;
   165: astore  6
   167: iconst_0
   168: istore  7
   170: goto    212
   173: getstatic       #82; //Field java/lang/System.out:Ljava/io/PrintStream;
   176: new     #88; //class java/lang/StringBuffer
   179: dup
   180: ldc     #117; //String fields[
   182: invokespecial   #92; //Method java/lang/StringBuffer."<init>":(Ljava/lang/String;)V
   185: iload   7
   187: invokevirtual   #93; //Method java/lang/StringBuffer.append:(I)Ljava/lang/StringBuffer;
   190: ldc     #97; //String ]:
   192: invokevirtual   #99; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
   195: aload   6
   197: iload   7
   199: aaload
   200: invokevirtual   #102; //Method java/lang/StringBuffer.append:(Ljava/lang/Object;)Ljava/lang/StringBuffer;
   203: invokevirtual   #105; //Method java/lang/StringBuffer.toString:()Ljava/lang/String;
   206: invokevirtual   #108; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   209: iinc    7, 1
   212: iload   7
   214: aload   6
   216: arraylength
   217: if_icmplt       173
   220: return
  Exception table:
   from   to  target type
    33    38    45   Class java/lang/ClassNotFoundException


public static void main(java.lang.String[])   throws java.lang.Exception;
  Code:
   0:   new     #1; //class Foo
   3:   dup
   4:   invokespecial   #136; //Method "<init>":()V
   7:   invokespecial   #137; //Method foo:()V
   10:  return

}

$ javap -c -private Foo$Bar
Compiled from "Foo.java"
class Foo$Bar extends java.lang.Object{
final Foo this$0;

private Foo$Bar(Foo);
  Code:
   0:   aload_0
   1:   invokespecial   #11; //Method java/lang/Object."<init>":()V
   4:   aload_0
   5:   aload_1
   6:   putfield        #14; //Field this$0:LFoo;
   9:   return

private void baz();
  Code:
   0:   getstatic       #21; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   ldc     #27; //String zap! zap! zap!
   5:   invokevirtual   #29; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   8:   return

Foo$Bar(Foo, Foo$Bar);
  Code:
   0:   aload_0
   1:   aload_1
   2:   invokespecial   #36; //Method "<init>":(LFoo;)V
   5:   return

static void access$1(Foo$Bar);
  Code:
   0:   aload_0
   1:   invokespecial   #40; //Method baz:()V
   4:   return

}

*1:実際にいつから使用できるのかは調べていません