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).
3.8 Identifiers
意訳:Java 文字には、'A-Z' と 'a-z'、また歴史的な理由によって、'_' と '$' が含まれる。
つまり、以下のようなコードは正しい 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
意訳:'$' は自動生成されたソースコードか、レガシーシステムにアクセスする場合のみ使用するのが望ましい。
話が少しそれますが、JSP や EJB が自動生成するコードは、先頭にアンダースコアが使用されることが多く、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:実際にいつから使用できるのかは調べていません