ネストされたクラスのコンパイル:javacおよびecj

ご存じのとおり、Java言語には、別のクラス内で宣言されたネストされたクラスがあります。 静的ネスト、 内部(内部)ローカル(ローカル)匿名(匿名)の 4つのタイプもあります(この記事では、Java 8で登場したラムダ式については触れません)。 これらはすべて、1つの興味深い機能によって結合されています。Java仮想マシンには、これらのクラスの特別なステータスに関する手がかりがありません。 彼女の観点から見ると、これらは外側のクラスと同じパッケージにある通常のクラスです。 ネストされたクラスを通常のクラスに変換する作業はすべて、コンパイラーにかかっています。 そして、ここでは、さまざまなコンパイラがそれをどのように扱っているかを見ることができます。 javac 1.8.0.20とEclipse JDT Core 3.10(eclipse Lunaにバンドルされています)のecjコンパイラーの動作を調べます。



ネストされたクラスのコンパイルに関連する主な問題は次のとおりです。



この記事では、最初の2つの問題について説明します。



アクセス権



アクセス権では、大きな手間がかかります。 ネストされたクラスのフィールドまたはメソッドをプライベートとして宣言できますが、Java仕様に従って、このフィールドまたはメソッドは引き続き外部クラスからアクセスできます。 その逆も可能です。ネストされたクラスから、または別のネストされたクラスから、外部クラスのプライベートフィールドまたはメソッドを参照して、別のクラスを使用します。 ただし、Javaマシンの観点からは、別のクラスのプライベートメンバーへのアクセスは受け入れられません。 同じことは、別のパッケージにある親クラスの保護されたメンバーへのアクセスにも当てはまります。 この制限を回避するために、コンパイラは特別なアクセス方法を作成します。 これらはすべて静的で、パッケージプライベートアクセスがあり、アクセス$で始まる名前が付けられています。 さらに、ecjは単にアクセス$ 0、アクセス$ 1などを呼び出し、javacは少なくとも3桁を追加します。最後の2つは特定の操作(読み取り= 00、書き込み= 02)をエンコードし、最初の操作はフィールドまたはメソッドです。 フィールドの読み取り、フィールドの書き込み、メソッドの呼び出しにはアクセス方法が必要です。



フィールドを読み取るためのアクセスメソッドには、1つのパラメーター-オブジェクト、およびフィールドを書き込むためのメソッド-2つのパラメーター(オブジェクトと新しい値)があります。 同時に、ecjでは記録メソッドはvoidを返し、javacでは新しい値(2番目のパラメーター)を返します。 たとえば、次のコードをご覧ください。



public class Outer { private int a; static class Nested { int b; void method(Outer i) { b = ia; ia = 5; } } }
      
      







Javacによって生成されたバイトコードをJavaに変換し直すと、次のようになります。

 public class Outer { private int a; static int access$000(Outer obj) { return obj.a; } static int access$002(Outer obj, int val) { return (obj.a = val); } } class Outer$Nested { int b; void method(Outer i) { b = Outer.access$000(i); Outer.access$002(i, 5); } }
      
      





ecjコードも同様で、メソッドはaccess $ 0、access $ 1と呼ばれ、2番目はvoidを返します。 privateという単語を削除すると、すべてがより簡単になります。アクセス方法は不要で、フィールドに直接アクセスできます。



興味深いことに、フィールドをインクリメントするとき、javacはよりスマートに動作します。 たとえば、次のコードをコンパイルします。

 public class Outer { private int a; static class Nested { void inc(Outer i) { i.a++; } } }
      
      





Javacは次のようなものを出力します。

 public class Outer { private int a; static int access$008(Outer obj) { return obj.a++; } } class Outer$Nested { void inc(Outer i) { Outer.access$008(i); } }
      
      





同様の動作が、デクリメント(メソッド名が10で終わる)、およびプリインクリメントとプリインクリメント(04および06)で観察されます。 これらすべての場合のecjコンパイラーは、最初にreadメソッドを呼び出し、次に1つを加算または減算し、writeメソッドを呼び出します。 奇数がどこに行ったのか誰かが興味を持っている場合、それらは外部クラスの親の保護フィールドへの直接アクセスで使用されます(たとえば、Outer.super.x = 2、これがどこで役立つかわかりません!)。



ちなみに、javac 1.7の動作がさらに賢くなり、タイプ+ =、<< =などの割り当て操作用の特別なメソッドが生成されることに興味があります(右側が計算され、生成されたメソッドに個別のパラメーターとして渡されます)。 アクセスできない文字列フィールドに+ =を適用した場合でも、特別なメソッドが生成されました。 javac 1.8では、この機能が故障しました。偶然のことです。 対応するコードがコンパイラのソースコードに存在するようです



プログラマー自身が適切な署名を使用してメソッドを作成する場合(たとえば、$ 000にアクセスして、絶対にしないでください!)、Javacは「シンボル(メソッド)が(クラス)のコンパイラー合成シンボルと競合します」というメッセージでファイルのコンパイルを拒否します。 ecjコンパイラーは、空きメソッド名が見つかるまでカウンターを増やすだけで、競合を静かに転送します。



アクセスできないメソッドを呼び出そうとすると、同じパラメーターと戻り値の型を持つ補助静的メソッドが作成され、追加のパラメーターのみがオブジェクトを渡すために追加されます。 より興味深い状況は、プライベートコンストラクターの使用です。 オブジェクトを構築するときは、コンストラクターを呼び出す必要があります。 したがって、コンパイラーは、目的のプライベートコンストラクターを呼び出す新しいプライベートコンストラクターを生成します。 署名によって既存のコンストラクタと競合しないコンストラクタを作成する方法は? Javacは、この目的のために新しいクラスを生成します! このコードを取ります:



 public class Outer { private Outer() {} static class Nested { void create() { new Outer(); } } }
      
      





コンパイルすると、Outer.classとOuter $ Nested.classだけでなく、別のOuterクラス$ 1.classが作成されます。 コンパイラによって生成されたコードは次のようになります。

 public class Outer { private Outer() {} Outer(Outer$1 ignore) { this(); } } class Outer$1 {} //      ,  ,     class Outer$Nested { void create() { new Outer((Outer$1)null); } }
      
      





このソリューションは、コンストラクターの署名をめぐる競合が保証されないという意味で便利です。 ecjコンパイラーは、余分なクラスを使用せずに、ダミーパラメーターを使用して同じクラスを追加することを決定しました。

 public class Outer { private Outer() {} Outer(Outer ignore) { this(); } } class Outer$Nested { void create() { new Outer((Outer)null); } }
      
      





既存のコンストラクターと競合する場合、新しいダミーパラメーターが追加されます。 たとえば、3つのコンストラクターがあります。

  private Outer() {} private Outer(Outer i1) {} private Outer(Outer i1, Outer i2) {}
      
      





ネストされたクラスからそれぞれを使用する場合、ecjは3つ、4つ、および5つの外部パラメーターを持つ3つの新しいクラスを作成します。



外部クラスのオブジェクトへの参照を渡す



内部クラス(ローカルおよび匿名を含む)は、外部クラスの特定のオブジェクトに関連付けられています。 これを実現するために、コンパイラは新しい最終フィールド(通常はこの$ 0という名前)を内部クラスに追加します。これには周囲のクラスへの参照が含まれます。 この場合、対応するパラメーターが各コンストラクターに追加されます。 このような単純なコードを使用する場合:

 public class Outer { class Nested {} void test() { new Nested(); } }
      
      





コンパイラ(ここではecjとjavacの動作は似ています)は、このコードを次のようなものに変換します(わかりやすくするために、手動でバイトコードを復元することを思い出します)。

 public class Outer { void test() { new Outer$Nested(this); } } class Outer$Nested { final Outer this$0; Outer$Nested(Outer obj) { this.this$0 = obj; super(); } }
      
      





興味深いことに、この$ 0の割り当ては、親クラスのコンストラクターを呼び出す前に発生します。 通常のJavaコードでは、親コンストラクターが実行されるまでフィールドに値を割り当てることはできませんが、バイトコードはこれを妨げません。 これにより、親クラスのコンストラクターによって呼び出されるメソッドをオーバーライドすると、この$ 0は既に初期化され、外部クラスのフィールドとメソッドに簡単にアクセスできます。



名前で競合を作成し、Nestedクラスにthis $ 0というフィールドがある場合(これは絶対にしないでください!)、これによりコンパイラが混乱することはありません。内部フィールドにこの$ 0 $という名前が付けられます。



Java言語を使用すると、これに基づいて内部クラスのインスタンスを作成できるだけでなく、同じタイプの別のオブジェクトにも基づいて作成できます。

 public class Outer { class Nested {} void test(Outer other) { other.new Nested(); } }
      
      





ここで興味深い点が生じます:結局のところ、他はヌルであることが判明する可能性があります。 良いことには、NullPointerExceptionでこの場所に落ちる必要があります。 通常、仮想マシン自体は、nullを間接参照しないことを保証しますが、Nestedオブジェクト内で外部クラスを使用するまで実際に間接参照することはありません。 コンパイラは再び抜け出す必要があります。偽の呼び出しを挿入し、コードを次のように変換します。

 public class Outer { void test(Outer other) { other.getClass(); new Outer$Nested(other); } }
      
      





getClass()の呼び出しは安全です。どのオブジェクトでも成功する必要があり、少し時間がかかります。 他のnullであることが判明した場合、Nestedオブジェクトの作成前でも例外が発生します。



クラスのネストレベルが複数の場合、新しい変数が最も内側の変数に表示されます:this $ 1など。 例として、これを考慮してください:



 public class Outer { class Nested { class SubNested { {test();} } } void test() { new Nested().new SubNested(); } }
      
      





ここで、javacは次のようなものを作成します。



 public class Outer { void test() { Outer$Nested tmp = new Outer$Nested(this); tmp.getClass(); //  ,   new Outer$Nested$SubNested(tmp); } } class Outer$Nested { final Outer this$0; Outer$Nested(Outer obj) { this.this$0 = obj; super(); } } class Outer$Nested$SubNested { final Outer$Nested this$1; Outer$Nested$SubNested(Outer$Nested obj) { this.this$1 = obj; super(); this.this$1.this$0.test(); } }
      
      





このオブジェクトを作成したばかりであるため、getClass()の呼び出しは削除できますが、コンパイラは気にしません。 しかし、ecjは通常、予期しないアクセス方法を生成しました。



 class Outer$Nested { final Outer this$0; Outer$Nested(Outer obj) { this.this$0 = obj; super(); } static Outer access$0(Outer$Nested obj) { return obj.this$0; } } class Outer$Nested$SubNested { final Outer$Nested this$1; Outer$Nested$SubNested(Outer$Nested obj) { this.this$1 = obj; super(); Outer$Nested.access$0(obj).test(); } }
      
      





この$ 0にはプライベートフラグがないため、非常に奇妙です。 一方、ecjはフィールドthis.this $ 1にアクセスする代わりにobjパラメーターを再利用することを推測しました。



結論



ネストされたクラスは、コンパイラの頭痛の種です。 パッケージプライベートアクセスを軽視しないでください。この場合、コンパイラは自動生成されたメソッドなしで実行します。 もちろん、最新の仮想マシンはほとんど常にインライン化されますが、これらのメソッドが存在すると、より多くのメモリが必要になり、クラス定数のプールが大きくなり、スタックトレースが長くなり、デバッグ時に余分な手順が追加されます。



コンパイラが異なれば、同様の状況で非常に異なるコードが生成される可能性があります。生成されるクラスの数でさえも異なります。 バイトコードを分析するためのツールを作成している場合、さまざまなコンパイラの動作を考慮する必要があります。



All Articles