Scala言語は、その優れた組み合わせのおかげで、過去数年にわたって人気を博し続けています。 関数型およびオブジェクト指向のソフトウェア開発の原則 、および実績のあるJava仮想マシン(JVM)上でのその実装。
でも はしご Javaバイトコードにコンパイルされ、Java言語の認識されている欠点の多くを改善するように設計されています。完全な関数型プログラミングサポートを提供するScalaのコア構文には、Javaプログラマーが明示的に構築する必要のある多くの暗黙的な構造が含まれており、その中にはかなり複雑なものもあります。
C ++でコーディングすることを学ぶ
Javaバイトコードにコンパイルする言語を作成するには、Java仮想マシンの内部動作を深く理解する必要があります。 Scalaの開発者が成し遂げたことを理解するには、内部を調べて、効率的で効果的なJVMバイトコードを生成するためにScalaのソースコードがコンパイラーによってどのように解釈されるかを調べる必要があります。
これらすべてがどのように実装されているかを見てみましょう。
この記事を読むには、Java仮想マシンのバイトコードの基本的な理解が必要です。完全な仮想マシンの仕様は、次のサイトから入手できます。 Oracleの公式ドキュメント 。この記事を理解するために仕様全体を読むことは重要ではないため、基本を簡単に紹介するために、記事の下部に短いガイドを用意しました。
JVMの基本に関するクラッシュコースを読むには、ここをクリックしてください。以下に示す例を再現するためにJavaバイトコードを逆アセンブルし、さらに調査を進めるには、ユーティリティが必要です。 Java Development Kitには、独自のコマンドラインユーティリティjavap
が用意されており、ここで使用します。 javap
の簡単なデモンストレーション作品が含まれています 下部のガイドで 。
そしてもちろん、例に沿ってフォローしたい読者には、Scalaコンパイラの実用的なインストールが必要です。この記事はを使用して書かれました スケール2.11.7 。 Scalaのバージョンが異なると、バイトコードがわずかに異なる場合があります。
Javaの規則では、パブリック属性のゲッターメソッドとセッターメソッドが常に提供されますが、Javaプログラマーは、それぞれのパターンが数十年にわたって変更されていないにもかかわらず、これらを自分で作成する必要があります。対照的に、Scalaはデフォルトのゲッターとセッターを提供します。
次の例を見てみましょう。
class Person(val name:String) { }
クラスの内部を見てみましょうPerson
。このファイルをscalac
でコンパイルすると、$ javap -p Person.class
が実行されます。私たちに与える:
Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }
Scalaクラスの各フィールドについて、フィールドとそのゲッターメソッドが生成されていることがわかります。フィールドはプライベートでファイナルですが、メソッドはパブリックです。
val
を置き換える場合var
でPerson
でソースを作成して再コンパイルしてから、フィールドのfinal
修飾子が削除され、setterメソッドも追加されます。
Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }
もしあればval
またはvar
がクラス本体内で定義されると、対応するプライベートフィールドとアクセサメソッドが作成され、インスタンスの作成時に適切に初期化されます。
クラスレベルのそのような実装に注意してくださいval
およびvar
フィールドは、いくつかの変数が中間値を格納するためにクラスレベルで使用され、プログラマーが直接アクセスしない場合、そのような各フィールドを初期化すると、クラスのフットプリントに1つから2つのメソッドが追加されることを意味します。 private
を追加するこのようなフィールドの修飾子は、対応するアクセサーが削除されることを意味するものではありません。彼らはただプライベートになります。
メソッドm()
があり、この関数への3つの異なるScalaスタイルの参照を作成するとします。
class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }
m
へのこれらの各参照はどうですか構築されましたか?いつm
いずれの場合も実行されますか?結果のバイトコードを見てみましょう。次の出力は、javap -v Person.class
の結果を示しています。 (多くの余分な出力を省略):
Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return
定数プールでは、メソッドm()
への参照がわかります。インデックス#30
に格納されます。コンストラクターコードでは、このメソッドが初期化中に2回呼び出され、命令invokevirtual #30
が使用されていることがわかります。最初にバイトオフセット11に表示され、次にオフセット19に表示されます。最初の呼び出しの後に命令putfield #22
が続きます。これは、このメソッドの結果を、インデックスm1
によって参照されるフィールド#22
に割り当てます。定数プールで。 2回目の呼び出しの後に同じパターンが続きます。今回は、m2
でインデックス付けされたフィールド#24
に値を割り当てます。定数プールで。
つまり、val
で定義された変数にメソッドを割り当てます。またはvar
のみを割り当てます 結果 その変数へのメソッドの。メソッドm1()
がわかりますおよびm2()
作成されるのは、これらの変数の単なるゲッターです。 var m2
の場合、セッターm2_$eq(int)
もわかります。が作成され、他のセッターと同じように動作し、フィールドの値を上書きします。
ただし、キーワードdef
を使用する別の結果が得られます。返すフィールド値をフェッチするのではなく、メソッドm3()
命令invokevirtual #30
も含まれています。つまり、このメソッドが呼び出されるたびに、m()
が呼び出され、このメソッドの結果が返されます。
したがって、ご覧のとおり、Scalaはクラスフィールドを操作する3つの方法を提供し、これらはキーワードval
、var
、およびdef
を介して簡単に指定できます。 Javaでは、必要なセッターとゲッターを明示的に実装する必要があり、そのような手動で記述されたボイラープレートコードは、表現力がはるかに低く、エラーが発生しやすくなります。
遅延値を宣言すると、より複雑なコードが生成されます。以前に定義したクラスに次のフィールドを追加したと仮定します。
lazy val m4 = m
実行中javap -p -v Person.class
これで、次のことが明らかになります。
Constant pool: #20 = Fieldref #2.#19 // Person.bitmapScalaJVMバイトコードで手を汚す
Scala言語は、その優れた組み合わせのおかげで、過去数年にわたって人気を博し続けています。 関数型およびオブジェクト指向のソフトウェア開発の原則 、および実績のあるJava仮想マシン(JVM)上でのその実装。
でも はしご Javaバイトコードにコンパイルされ、Java言語の認識されている欠点の多くを改善するように設計されています。完全な関数型プログラミングサポートを提供するScalaのコア構文には、Javaプログラマーが明示的に構築する必要のある多くの暗黙的な構造が含まれており、その中にはかなり複雑なものもあります。
Javaバイトコードにコンパイルする言語を作成するには、Java仮想マシンの内部動作を深く理解する必要があります。 Scalaの開発者が成し遂げたことを理解するには、内部を調べて、効率的で効果的なJVMバイトコードを生成するためにScalaのソースコードがコンパイラーによってどのように解釈されるかを調べる必要があります。
これらすべてがどのように実装されているかを見てみましょう。
この記事を読むには、Java仮想マシンのバイトコードの基本的な理解が必要です。完全な仮想マシンの仕様は、次のサイトから入手できます。 Oracleの公式ドキュメント 。この記事を理解するために仕様全体を読むことは重要ではないため、基本を簡単に紹介するために、記事の下部に短いガイドを用意しました。
JVMの基本に関するクラッシュコースを読むには、ここをクリックしてください。以下に示す例を再現するためにJavaバイトコードを逆アセンブルし、さらに調査を進めるには、ユーティリティが必要です。 Java Development Kitには、独自のコマンドラインユーティリティjavap
が用意されており、ここで使用します。 javap
の簡単なデモンストレーション作品が含まれています 下部のガイドで 。
そしてもちろん、例に沿ってフォローしたい読者には、Scalaコンパイラの実用的なインストールが必要です。この記事はを使用して書かれました スケール2.11.7 。 Scalaのバージョンが異なると、バイトコードがわずかに異なる場合があります。
Javaの規則では、パブリック属性のゲッターメソッドとセッターメソッドが常に提供されますが、Javaプログラマーは、それぞれのパターンが数十年にわたって変更されていないにもかかわらず、これらを自分で作成する必要があります。対照的に、Scalaはデフォルトのゲッターとセッターを提供します。
次の例を見てみましょう。
class Person(val name:String) { }
クラスの内部を見てみましょうPerson
。このファイルをscalac
でコンパイルすると、$ javap -p Person.class
が実行されます。私たちに与える:
Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }
Scalaクラスの各フィールドについて、フィールドとそのゲッターメソッドが生成されていることがわかります。フィールドはプライベートでファイナルですが、メソッドはパブリックです。
val
を置き換える場合var
でPerson
でソースを作成して再コンパイルしてから、フィールドのfinal
修飾子が削除され、setterメソッドも追加されます。
Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }
もしあればval
またはvar
がクラス本体内で定義されると、対応するプライベートフィールドとアクセサメソッドが作成され、インスタンスの作成時に適切に初期化されます。
クラスレベルのそのような実装に注意してくださいval
およびvar
フィールドは、いくつかの変数が中間値を格納するためにクラスレベルで使用され、プログラマーが直接アクセスしない場合、そのような各フィールドを初期化すると、クラスのフットプリントに1つから2つのメソッドが追加されることを意味します。 private
を追加するこのようなフィールドの修飾子は、対応するアクセサーが削除されることを意味するものではありません。彼らはただプライベートになります。
メソッドm()
があり、この関数への3つの異なるScalaスタイルの参照を作成するとします。
class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }
m
へのこれらの各参照はどうですか構築されましたか?いつm
いずれの場合も実行されますか?結果のバイトコードを見てみましょう。次の出力は、javap -v Person.class
の結果を示しています。 (多くの余分な出力を省略):
Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return
定数プールでは、メソッドm()
への参照がわかります。インデックス#30
に格納されます。コンストラクターコードでは、このメソッドが初期化中に2回呼び出され、命令invokevirtual #30
が使用されていることがわかります。最初にバイトオフセット11に表示され、次にオフセット19に表示されます。最初の呼び出しの後に命令putfield #22
が続きます。これは、このメソッドの結果を、インデックスm1
によって参照されるフィールド#22
に割り当てます。定数プールで。 2回目の呼び出しの後に同じパターンが続きます。今回は、m2
でインデックス付けされたフィールド#24
に値を割り当てます。定数プールで。
つまり、val
で定義された変数にメソッドを割り当てます。またはvar
のみを割り当てます 結果 その変数へのメソッドの。メソッドm1()
がわかりますおよびm2()
作成されるのは、これらの変数の単なるゲッターです。 var m2
の場合、セッターm2_$eq(int)
もわかります。が作成され、他のセッターと同じように動作し、フィールドの値を上書きします。
ただし、キーワードdef
を使用する別の結果が得られます。返すフィールド値をフェッチするのではなく、メソッドm3()
命令invokevirtual #30
も含まれています。つまり、このメソッドが呼び出されるたびに、m()
が呼び出され、このメソッドの結果が返されます。
したがって、ご覧のとおり、Scalaはクラスフィールドを操作する3つの方法を提供し、これらはキーワードval
、var
、およびdef
を介して簡単に指定できます。 Javaでは、必要なセッターとゲッターを明示的に実装する必要があり、そのような手動で記述されたボイラープレートコードは、表現力がはるかに低く、エラーが発生しやすくなります。
遅延値を宣言すると、より複雑なコードが生成されます。以前に定義したクラスに次のフィールドを追加したと仮定します。
lazy val m4 = m
実行中javap -p -v Person.class
これで、次のことが明らかになります。
Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn
この場合、フィールドの値m4
必要になるまで計算されません。特別なプライベートメソッドm4$lzycompute()
レイジー値を計算するために生成され、フィールドbitmap$0
その状態を追跡します。メソッドm4()
このフィールドの値が0であるかどうかをチェックし、m4
であることを示します。まだ初期化されていません。この場合、m4$lzycompute()
が呼び出され、m4
が入力されますそしてその値を返します。このプライベートメソッドは、bitmap$0
の値も設定します次回はm4()
が呼び出されると、初期化メソッドの呼び出しがスキップされ、代わりにm4
の値が返されます。
Scalaがここで生成するバイトコードは、スレッドセーフで効果的であるように設計されています。スレッドセーフにするために、レイジーコンピューティングメソッドはmonitorenter
/ monitorexit
を使用します。指示のペア。この同期のパフォーマンスオーバーヘッドは、遅延値の最初の読み取り時にのみ発生するため、この方法は引き続き有効です。
遅延値の状態を示すために必要なビットは1つだけです。したがって、レイジー値が32個以下の場合、1つのintフィールドでそれらすべてを追跡できます。ソースコードで複数の遅延値が定義されている場合、上記のバイトコードは、この目的のためにビットマスクを実装するようにコンパイラによって変更されます。
繰り返しになりますが、Scalaを使用すると、Javaで明示的に実装する必要がある特定の種類の動作を簡単に利用できるため、労力を節約し、タイプミスのリスクを減らすことができます。
それでは、次のScalaソースコードを見てみましょう。
class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }
Printer
クラスには、タイプoutput
の1つのフィールドString => Unit
があります。String
を受け取る関数です。タイプUnit
のオブジェクトを返します(Javaのvoid
に似ています)。 mainメソッドでは、これらのオブジェクトの1つを作成し、このフィールドを指定された文字列を出力する無名関数として割り当てます。
このコードをコンパイルすると、4つのクラスファイルが生成されます。
Hello.class
mainメソッドが単にHello$.main()
を呼び出すラッパークラスです。
public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return
隠されたHello$.class
mainメソッドの実際の実装が含まれています。そのバイトコードを確認するには、$
を正しくエスケープしていることを確認してください。コマンドシェルの規則に従って、特殊文字としての解釈を避けるために:
public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return
このメソッドはPrinter
を作成します。次に、匿名関数Hello$$anonfun$1
を含むs => println(s)
を作成します。 Printer
このオブジェクトでoutput
として初期化されますフィールド。次に、このフィールドはスタックにロードされ、オペランド'Hello'
で実行されます。
以下の無名関数クラスHello$$anonfun$1.class
を見てみましょう。 ScalaのFunction1
を拡張していることがわかります(AbstractFunction1
として)apply()
を実装する方法。実際には、2つのapply()
を作成しますメソッドは、一方が他方をラップし、一緒に型チェックを実行し(この場合、入力がString
であること)、無名関数を実行します(入力をprintln()
で出力します)。
public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn
Hello$.main()
を振り返って上記の方法では、オフセット21で、無名関数の実行がそのapply( Object )
の呼び出しによってトリガーされることがわかります。方法。
最後に、完全を期すために、Printer.class
のバイトコードを見てみましょう。
public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return
ここでの無名関数は他のval
と同じように扱われることがわかります。変数。クラスフィールドoutput
とゲッターoutput()
に格納されます創造された。唯一の違いは、この変数がScalaインターフェースを実装する必要があることですscala.Function1
(これはAbstractFunction1
が行います)。
したがって、このエレガントなScala機能のコストは、値として使用できる単一の無名関数を表現および実行するために作成された、基礎となるユーティリティクラスです。特定のアプリケーションにとってそれが何を意味するのかを理解するには、そのような機能の数とVM実装の詳細を考慮する必要があります。
Scalaの内部を理解する:この強力な言語がJVMバイトコードでどのように実装されているかを探ります。 つぶやきScalaの特性は、Javaのインターフェースに似ています。次の特性は、2つのメソッド署名を定義し、2番目のメソッドのデフォルトの実装を提供します。それがどのように実装されているか見てみましょう:
trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }
2つのエンティティが生成されます。両方のメソッドを宣言するインターフェイスであるSimilarity.class
と、デフォルトの実装を提供する合成クラスSimilarity$class.class
です。
public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); }
public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return
クラスがこの特性を実装し、メソッドisNotSimilar
を呼び出すと、Scalaコンパイラーはバイトコード命令invokestatic
を生成します。付随するクラスによって提供される静的メソッドを呼び出します。
複雑なポリモーフィズムと継承構造は、特性から作成される場合があります。たとえば、複数のトレイトと実装クラスがすべて、同じシグネチャを持つメソッドをオーバーライドして、super.methodName()
を呼び出す場合があります。次の特性に制御を渡すため。 Scalaコンパイラがそのような呼び出しに遭遇すると、次のようになります。
invokestatic
を生成します命令。したがって、トレイトの強力な概念が、大きなオーバーヘッドを引き起こさない方法でJVMレベルで実装されていることがわかります。また、Scalaプログラマーは、実行時にコストがかかりすぎることを心配せずにこの機能を楽しむことができます。
Scalaは、キーワードobject
を使用してシングルトンクラスの明示的な定義を提供します。次のシングルトンクラスについて考えてみましょう。
object Config { val home_dir = '/home/user' }
コンパイラは2つのクラスファイルを生成します。
Config.class
非常に単純なものです:
public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn
これは、合成のデコレータですConfig$
シングルトンの機能を組み込むクラス。 javap -p -c
でそのクラスを調べる次のバイトコードを生成します。
public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return
以下で構成されています。
MODULE$
。{}
(クラス初期化子とも呼ばれます)およびConfig$
の初期化に使用されるプライベートメソッドMODULE$
フィールドをデフォルト値に設定しますhome_dir
のゲッターメソッド。この場合、それは1つの方法にすぎません。シングルトンにさらに多くのフィールドがある場合、可変フィールドのセッターだけでなく、より多くのゲッターがあります。シングルトンは人気があり便利なデザインパターンです。 Java言語は、言語レベルでそれを指定する直接的な方法を提供しません。むしろ、Javaソースに実装するのは開発者の責任です。一方、Scalaは、object
を使用してシングルトンを明示的に宣言するための明確で便利な方法を提供します。キーワード。内部を見るとわかるように、手頃な価格で自然な方法で実装されています。
これで、Scalaがいくつかの暗黙的および関数型プログラミング機能を洗練されたJavaバイトコード構造にコンパイルする方法を見てきました。 Scalaの内部の仕組みを垣間見ることで、Scalaの力をより深く理解することができ、この強力な言語を最大限に活用するのに役立ちます。
また、自分たちで言語を探索するためのツールもあります。ケースクラス、カリー化、リスト内包表記など、この記事では取り上げていないScala構文の便利な機能がたくさんあります。これらの構造のScalaの実装を自分で調査して、次のレベルのScala忍者になる方法を学ぶことをお勧めします。
Javaコンパイラと同様に、Scalaコンパイラはソースコードを.class
に変換します。 Java仮想マシンによって実行されるJavaバイトコードを含むファイル。 2つの言語が内部でどのように異なるかを理解するには、両方が対象としているシステムを理解する必要があります。ここでは、Java仮想マシンアーキテクチャのいくつかの主要な要素、クラスファイル構造、およびアセンブラの基本について簡単に説明します。
このガイドでは、上記の記事に沿ってフォローできるようにするための最小限の内容のみを取り上げていることに注意してください。 JVMの多くの主要コンポーネントについてはここでは説明していませんが、完全な詳細は公式ドキュメントにあります。 ここに 。
javap
を使用したクラスファイルの逆コンパイル
コンスタントプール
フィールドテーブルとメソッドテーブル
JVMバイトコード
メソッド呼び出しと呼び出しスタック
オペランドスタックでの実行
ローカル変数
トップに戻る
javap
を使用したクラスファイルの逆コンパイルJavaにはjavap
が付属しています.class
を逆コンパイルするコマンドラインユーティリティ人間が読める形式にファイルします。 ScalaクラスファイルとJavaクラスファイルはどちらも同じJVMを対象としているため、javap
Scalaによってコンパイルされたクラスファイルを調べるために使用できます。
次のソースコードをコンパイルしましょう。
// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }
これをscalac RegularPolygon.scala
でコンパイルするRegularPolygon.class
を生成します。次にjavap RegularPolygon.class
を実行すると次のように表示されます。
$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
これはクラスファイルの非常に単純な内訳であり、クラスのパブリックメンバーの名前とタイプを単純に示しています。 -p
を追加するオプションにはプライベートメンバーが含まれます:
$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
これはまだ多くの情報ではありません。メソッドがJavaバイトコードでどのように実装されているかを確認するために、-c
を追加しましょう。オプション:
$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }
それはもう少し面白いです。ただし、実際に全体像を把握するには、-v
を使用する必要があります。または-verbose
javap -p -v RegularPolygon.class
のようなオプション:
ここで、最終的にクラスファイルに実際に何が含まれているかがわかります。これはどういう意味ですか?最も重要な部分のいくつかを見てみましょう。
C ++アプリケーションの開発サイクルには、コンパイルとリンケージの段階が含まれます。リンケージは実行時に発生するため、Javaの開発サイクルは明示的なリンケージステージをスキップします。クラスファイルは、このランタイムリンクをサポートする必要があります。つまり、ソースコードが任意のフィールドまたはメソッドを参照する場合、結果のバイトコードは関連する参照をシンボリック形式で保持し、アプリケーションがメモリにロードされ、ランタイムリンカーによって実際のアドレスを解決できるようになったら逆参照できるようにする必要があります。このシンボリックフォームには、次のものが含まれている必要があります。
クラスファイル形式の仕様には、ファイルの「 定数プール 、リンカが必要とするすべての参照の表。さまざまなタイプのエントリが含まれています。
// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...
各エントリの最初のバイトは、エントリのタイプを示す数値タグです。残りのバイトは、エントリの値に関する情報を提供します。バイト数とその解釈の規則は、最初のバイトで示されるタイプによって異なります。
たとえば、定数整数を使用するJavaクラス365
次のバイトコードを持つ定数プールエントリがある場合があります。
x03 00 00 01 6D
最初のバイトx03
は、エントリタイプCONSTANT_Integer
を識別します。これは、次の4バイトに整数の値が含まれていることをリンカに通知します。 (16進数の365はx16D
であることに注意してください)。これが定数プールの14番目のエントリである場合、javap -v
次のようにレンダリングされます:
#14 = Integer 365
多くの定数型は、定数プール内の他の場所にある、より「プリミティブな」定数型への参照で構成されています。たとえば、サンプルコードには次のステートメントが含まれています。
println( 'Calculating perimeter...' )
文字列定数を使用すると、定数プールに2つのエントリが生成されます。1つはタイプCONSTANT_String
のエントリです。 、およびタイプCONSTANT_Utf8
の別のエントリ。タイプConstant_UTF8
のエントリ文字列値の実際のUTF8表現が含まれます。タイプCONSTANT_String
のエントリCONSTANT_Utf8
への参照が含まれていますエントリ:
#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...
タイプUtf8
のエントリを参照する他のタイプの定数プールエントリがあるため、このような複雑さが必要です。タイプString
のエントリではありません。たとえば、クラス属性を参照すると、CONSTANT_Fieldref
が生成されます。タイプ。クラス名、属性名、および属性タイプへの一連の参照が含まれています。
#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I
定数プールの詳細については、を参照してください。 JVMドキュメント 。
クラスファイルには、 フィールドテーブル クラスで定義された各フィールド(つまり、属性)に関する情報が含まれています。これらは、フィールドの名前とタイプ、およびアクセス制御フラグとその他の関連データを説明する定数プールエントリへの参照です。
似たような メソッドテーブル クラスファイルに存在します。ただし、名前とタイプの情報に加えて、非抽象メソッドごとに、JVMによって実行される実際のバイトコード命令と、以下で説明するメソッドのスタックフレームによって使用されるデータ構造が含まれます。
JVMは、独自の内部命令セットを使用して、コンパイルされたコードを実行します。実行中javap
-c
でオプションは、コンパイルされたメソッドの実装を出力に含めます。 RegularPolygon.class
を調べるとこの方法でファイルすると、getPerimeter()
の次の出力が表示されます。方法:
public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn
実際のバイトコードは次のようになります。
xB2 00 17 x12 19 xB6 00 1D x27 ...
各命令は1バイトで始まります オペコード 特定の命令の形式に応じて、JVM命令を識別し、その後に操作対象の0個以上の命令オペランドを指定します。これらは通常、定数値、または定数プールへの参照のいずれかです。 javap
バイトコードを人間が読める形式に変換すると、次のように表示されます。
#23
などのポンド記号で表示されるオペランドは、定数プール内のエントリーへの参照です。ご覧のとおり、javap
また、出力に役立つコメントを生成し、プールから正確に参照されているものを識別します。
以下では、いくつかの一般的な手順について説明します。完全なJVM命令セットの詳細については、 ドキュメンテーション 。
各メソッド呼び出しは、ローカルで宣言された変数やメソッドに渡された引数などを含む、独自のコンテキストで実行できる必要があります。一緒に、これらは構成します スタックフレーム 。メソッドを呼び出すと、新しいフレームが作成され、その上に配置されます。 コールスタック 。メソッドが戻ると、現在のフレームが呼び出しスタックから削除されて破棄され、メソッドが呼び出される前に有効だったフレームが復元されます。
スタックフレームには、いくつかの異なる構造が含まれています。 2つの重要なものは オペランドスタック そしてその ローカル変数テーブル 、次に説明します。
多くのJVM命令は、フレームで動作します オペランドスタック 。これらの命令は、バイトコードで定数オペランドを明示的に指定するのではなく、オペランドスタックの最上位の値を入力として受け取ります。通常、これらの値はプロセスでスタックから削除されます。一部の命令では、スタックの最上位に新しい値も配置されます。このようにして、JVM命令を組み合わせて、複雑な操作を実行できます。たとえば、次の式です。
sideLength * this.numSides
getPerimeter()
で次のようにコンパイルされます方法:
8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul
dload_1
は、オブジェクト参照をローカル変数テーブル(次に説明)のスロット1からオペランドスタックにプッシュします。この場合、これはメソッド引数sideLength
.-次の命令aload_0
は、ローカル変数テーブルのスロット0にあるオブジェクト参照をオペランドスタックにプッシュします。実際には、これはほとんどの場合、現在のクラスであるthis
への参照です。invokevirtual #31
を実行する次の呼び出しnumSides()
のスタックが設定されます。 invokevirtual
最上位のオペランド(this
への参照)をスタックからポップして、メソッドを呼び出す必要のあるクラスを識別します。メソッドが戻ると、その結果はスタックにプッシュされます。numSides
)は整数形式です。別のdouble値を乗算するには、double浮動小数点形式に変換する必要があります。命令i2d
整数値をスタックからポップし、浮動小数点形式に変換して、スタックにプッシュバックします。this.numSides
の浮動小数点結果が含まれています。上にsideLength
の値が続きますメソッドに渡された引数。 dmul
これらの上位2つの値をスタックからポップし、それらに対して浮動小数点乗算を実行して、結果をスタックにプッシュします。メソッドが呼び出されると、そのスタックフレームの一部として新しいオペランドスタックが作成され、そこで操作が実行されます。ここでは用語に注意する必要があります。「スタック」という言葉は、 コールスタック 、メソッド実行または特定のフレームのコンテキストを提供するフレームのスタック オペランドスタック 、JVM命令が動作する。
各スタックフレームは、 ローカル変数 。これには通常、this
への参照が含まれますオブジェクト、メソッドが呼び出されたときに渡された引数、およびメソッド本体内で宣言されたローカル変数。実行中javap
-v
でオプションには、ローカル変数テーブルなど、各メソッドのスタックフレームの設定方法に関する情報が含まれます。
public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D
この例では、2つのローカル変数があります。スロット0の変数の名前はthis
で、タイプはRegularPolygon
です。これは、メソッド自体のクラスへの参照です。スロット1の変数の名前はsideLength
で、タイプはD
です。 (ダブルを示します)。これは、getPerimeter()
に渡される引数です。方法。
iload_1
、fstore_2
、aload [n]
などの命令は、オペランドスタックとローカル変数テーブルの間でさまざまなタイプのローカル変数を転送します。通常、表の最初の項目はthis
への参照であるため、命令aload_0
独自のクラスで動作するすべてのメソッドで一般的に見られます。
これで、JVMの基本についてのウォークスルーは終わりです。 メインの記事に戻るには、ここをクリックしてください。
Scala言語は、その優れた組み合わせのおかげで、過去数年にわたって人気を博し続けています。 関数型およびオブジェクト指向のソフトウェア開発の原則 、および実績のあるJava仮想マシン(JVM)上でのその実装。
でも はしご Javaバイトコードにコンパイルされ、Java言語の認識されている欠点の多くを改善するように設計されています。完全な関数型プログラミングサポートを提供するScalaのコア構文には、Javaプログラマーが明示的に構築する必要のある多くの暗黙的な構造が含まれており、その中にはかなり複雑なものもあります。
Javaバイトコードにコンパイルする言語を作成するには、Java仮想マシンの内部動作を深く理解する必要があります。 Scalaの開発者が成し遂げたことを理解するには、内部を調べて、効率的で効果的なJVMバイトコードを生成するためにScalaのソースコードがコンパイラーによってどのように解釈されるかを調べる必要があります。
これらすべてがどのように実装されているかを見てみましょう。
この記事を読むには、Java仮想マシンのバイトコードの基本的な理解が必要です。完全な仮想マシンの仕様は、次のサイトから入手できます。 Oracleの公式ドキュメント 。この記事を理解するために仕様全体を読むことは重要ではないため、基本を簡単に紹介するために、記事の下部に短いガイドを用意しました。
JVMの基本に関するクラッシュコースを読むには、ここをクリックしてください。以下に示す例を再現するためにJavaバイトコードを逆アセンブルし、さらに調査を進めるには、ユーティリティが必要です。 Java Development Kitには、独自のコマンドラインユーティリティjavap
が用意されており、ここで使用します。 javap
の簡単なデモンストレーション作品が含まれています 下部のガイドで 。
そしてもちろん、例に沿ってフォローしたい読者には、Scalaコンパイラの実用的なインストールが必要です。この記事はを使用して書かれました スケール2.11.7 。 Scalaのバージョンが異なると、バイトコードがわずかに異なる場合があります。
Javaの規則では、パブリック属性のゲッターメソッドとセッターメソッドが常に提供されますが、Javaプログラマーは、それぞれのパターンが数十年にわたって変更されていないにもかかわらず、これらを自分で作成する必要があります。対照的に、Scalaはデフォルトのゲッターとセッターを提供します。
次の例を見てみましょう。
class Person(val name:String) { }
クラスの内部を見てみましょうPerson
。このファイルをscalac
でコンパイルすると、$ javap -p Person.class
が実行されます。私たちに与える:
Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }
Scalaクラスの各フィールドについて、フィールドとそのゲッターメソッドが生成されていることがわかります。フィールドはプライベートでファイナルですが、メソッドはパブリックです。
val
を置き換える場合var
でPerson
でソースを作成して再コンパイルしてから、フィールドのfinal
修飾子が削除され、setterメソッドも追加されます。
Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }
もしあればval
またはvar
がクラス本体内で定義されると、対応するプライベートフィールドとアクセサメソッドが作成され、インスタンスの作成時に適切に初期化されます。
クラスレベルのそのような実装に注意してくださいval
およびvar
フィールドは、いくつかの変数が中間値を格納するためにクラスレベルで使用され、プログラマーが直接アクセスしない場合、そのような各フィールドを初期化すると、クラスのフットプリントに1つから2つのメソッドが追加されることを意味します。 private
を追加するこのようなフィールドの修飾子は、対応するアクセサーが削除されることを意味するものではありません。彼らはただプライベートになります。
メソッドm()
があり、この関数への3つの異なるScalaスタイルの参照を作成するとします。
class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }
m
へのこれらの各参照はどうですか構築されましたか?いつm
いずれの場合も実行されますか?結果のバイトコードを見てみましょう。次の出力は、javap -v Person.class
の結果を示しています。 (多くの余分な出力を省略):
Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return
定数プールでは、メソッドm()
への参照がわかります。インデックス#30
に格納されます。コンストラクターコードでは、このメソッドが初期化中に2回呼び出され、命令invokevirtual #30
が使用されていることがわかります。最初にバイトオフセット11に表示され、次にオフセット19に表示されます。最初の呼び出しの後に命令putfield #22
が続きます。これは、このメソッドの結果を、インデックスm1
によって参照されるフィールド#22
に割り当てます。定数プールで。 2回目の呼び出しの後に同じパターンが続きます。今回は、m2
でインデックス付けされたフィールド#24
に値を割り当てます。定数プールで。
つまり、val
で定義された変数にメソッドを割り当てます。またはvar
のみを割り当てます 結果 その変数へのメソッドの。メソッドm1()
がわかりますおよびm2()
作成されるのは、これらの変数の単なるゲッターです。 var m2
の場合、セッターm2_$eq(int)
もわかります。が作成され、他のセッターと同じように動作し、フィールドの値を上書きします。
ただし、キーワードdef
を使用する別の結果が得られます。返すフィールド値をフェッチするのではなく、メソッドm3()
命令invokevirtual #30
も含まれています。つまり、このメソッドが呼び出されるたびに、m()
が呼び出され、このメソッドの結果が返されます。
したがって、ご覧のとおり、Scalaはクラスフィールドを操作する3つの方法を提供し、これらはキーワードval
、var
、およびdef
を介して簡単に指定できます。 Javaでは、必要なセッターとゲッターを明示的に実装する必要があり、そのような手動で記述されたボイラープレートコードは、表現力がはるかに低く、エラーが発生しやすくなります。
遅延値を宣言すると、より複雑なコードが生成されます。以前に定義したクラスに次のフィールドを追加したと仮定します。
lazy val m4 = m
実行中javap -p -v Person.class
これで、次のことが明らかになります。
Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn
この場合、フィールドの値m4
必要になるまで計算されません。特別なプライベートメソッドm4$lzycompute()
レイジー値を計算するために生成され、フィールドbitmap$0
その状態を追跡します。メソッドm4()
このフィールドの値が0であるかどうかをチェックし、m4
であることを示します。まだ初期化されていません。この場合、m4$lzycompute()
が呼び出され、m4
が入力されますそしてその値を返します。このプライベートメソッドは、bitmap$0
の値も設定します次回はm4()
が呼び出されると、初期化メソッドの呼び出しがスキップされ、代わりにm4
の値が返されます。
Scalaがここで生成するバイトコードは、スレッドセーフで効果的であるように設計されています。スレッドセーフにするために、レイジーコンピューティングメソッドはmonitorenter
/ monitorexit
を使用します。指示のペア。この同期のパフォーマンスオーバーヘッドは、遅延値の最初の読み取り時にのみ発生するため、この方法は引き続き有効です。
遅延値の状態を示すために必要なビットは1つだけです。したがって、レイジー値が32個以下の場合、1つのintフィールドでそれらすべてを追跡できます。ソースコードで複数の遅延値が定義されている場合、上記のバイトコードは、この目的のためにビットマスクを実装するようにコンパイラによって変更されます。
繰り返しになりますが、Scalaを使用すると、Javaで明示的に実装する必要がある特定の種類の動作を簡単に利用できるため、労力を節約し、タイプミスのリスクを減らすことができます。
それでは、次のScalaソースコードを見てみましょう。
class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }
Printer
クラスには、タイプoutput
の1つのフィールドString => Unit
があります。String
を受け取る関数です。タイプUnit
のオブジェクトを返します(Javaのvoid
に似ています)。 mainメソッドでは、これらのオブジェクトの1つを作成し、このフィールドを指定された文字列を出力する無名関数として割り当てます。
このコードをコンパイルすると、4つのクラスファイルが生成されます。
Hello.class
mainメソッドが単にHello$.main()
を呼び出すラッパークラスです。
public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return
隠されたHello$.class
mainメソッドの実際の実装が含まれています。そのバイトコードを確認するには、$
を正しくエスケープしていることを確認してください。コマンドシェルの規則に従って、特殊文字としての解釈を避けるために:
public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return
このメソッドはPrinter
を作成します。次に、匿名関数Hello$$anonfun$1
を含むs => println(s)
を作成します。 Printer
このオブジェクトでoutput
として初期化されますフィールド。次に、このフィールドはスタックにロードされ、オペランド'Hello'
で実行されます。
以下の無名関数クラスHello$$anonfun$1.class
を見てみましょう。 ScalaのFunction1
を拡張していることがわかります(AbstractFunction1
として)apply()
を実装する方法。実際には、2つのapply()
を作成しますメソッドは、一方が他方をラップし、一緒に型チェックを実行し(この場合、入力がString
であること)、無名関数を実行します(入力をprintln()
で出力します)。
public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn
Hello$.main()
を振り返って上記の方法では、オフセット21で、無名関数の実行がそのapply( Object )
の呼び出しによってトリガーされることがわかります。方法。
最後に、完全を期すために、Printer.class
のバイトコードを見てみましょう。
public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return
ここでの無名関数は他のval
と同じように扱われることがわかります。変数。クラスフィールドoutput
とゲッターoutput()
に格納されます創造された。唯一の違いは、この変数がScalaインターフェースを実装する必要があることですscala.Function1
(これはAbstractFunction1
が行います)。
したがって、このエレガントなScala機能のコストは、値として使用できる単一の無名関数を表現および実行するために作成された、基礎となるユーティリティクラスです。特定のアプリケーションにとってそれが何を意味するのかを理解するには、そのような機能の数とVM実装の詳細を考慮する必要があります。
Scalaの内部を理解する:この強力な言語がJVMバイトコードでどのように実装されているかを探ります。 つぶやきScalaの特性は、Javaのインターフェースに似ています。次の特性は、2つのメソッド署名を定義し、2番目のメソッドのデフォルトの実装を提供します。それがどのように実装されているか見てみましょう:
trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }
2つのエンティティが生成されます。両方のメソッドを宣言するインターフェイスであるSimilarity.class
と、デフォルトの実装を提供する合成クラスSimilarity$class.class
です。
public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); }
public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return
クラスがこの特性を実装し、メソッドisNotSimilar
を呼び出すと、Scalaコンパイラーはバイトコード命令invokestatic
を生成します。付随するクラスによって提供される静的メソッドを呼び出します。
複雑なポリモーフィズムと継承構造は、特性から作成される場合があります。たとえば、複数のトレイトと実装クラスがすべて、同じシグネチャを持つメソッドをオーバーライドして、super.methodName()
を呼び出す場合があります。次の特性に制御を渡すため。 Scalaコンパイラがそのような呼び出しに遭遇すると、次のようになります。
invokestatic
を生成します命令。したがって、トレイトの強力な概念が、大きなオーバーヘッドを引き起こさない方法でJVMレベルで実装されていることがわかります。また、Scalaプログラマーは、実行時にコストがかかりすぎることを心配せずにこの機能を楽しむことができます。
Scalaは、キーワードobject
を使用してシングルトンクラスの明示的な定義を提供します。次のシングルトンクラスについて考えてみましょう。
object Config { val home_dir = '/home/user' }
コンパイラは2つのクラスファイルを生成します。
Config.class
非常に単純なものです:
public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn
これは、合成のデコレータですConfig$
シングルトンの機能を組み込むクラス。 javap -p -c
でそのクラスを調べる次のバイトコードを生成します。
public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return
以下で構成されています。
MODULE$
。{}
(クラス初期化子とも呼ばれます)およびConfig$
の初期化に使用されるプライベートメソッドMODULE$
フィールドをデフォルト値に設定しますhome_dir
のゲッターメソッド。この場合、それは1つの方法にすぎません。シングルトンにさらに多くのフィールドがある場合、可変フィールドのセッターだけでなく、より多くのゲッターがあります。シングルトンは人気があり便利なデザインパターンです。 Java言語は、言語レベルでそれを指定する直接的な方法を提供しません。むしろ、Javaソースに実装するのは開発者の責任です。一方、Scalaは、object
を使用してシングルトンを明示的に宣言するための明確で便利な方法を提供します。キーワード。内部を見るとわかるように、手頃な価格で自然な方法で実装されています。
これで、Scalaがいくつかの暗黙的および関数型プログラミング機能を洗練されたJavaバイトコード構造にコンパイルする方法を見てきました。 Scalaの内部の仕組みを垣間見ることで、Scalaの力をより深く理解することができ、この強力な言語を最大限に活用するのに役立ちます。
また、自分たちで言語を探索するためのツールもあります。ケースクラス、カリー化、リスト内包表記など、この記事では取り上げていないScala構文の便利な機能がたくさんあります。これらの構造のScalaの実装を自分で調査して、次のレベルのScala忍者になる方法を学ぶことをお勧めします。
Javaコンパイラと同様に、Scalaコンパイラはソースコードを.class
に変換します。 Java仮想マシンによって実行されるJavaバイトコードを含むファイル。 2つの言語が内部でどのように異なるかを理解するには、両方が対象としているシステムを理解する必要があります。ここでは、Java仮想マシンアーキテクチャのいくつかの主要な要素、クラスファイル構造、およびアセンブラの基本について簡単に説明します。
このガイドでは、上記の記事に沿ってフォローできるようにするための最小限の内容のみを取り上げていることに注意してください。 JVMの多くの主要コンポーネントについてはここでは説明していませんが、完全な詳細は公式ドキュメントにあります。 ここに 。
javap
を使用したクラスファイルの逆コンパイル
コンスタントプール
フィールドテーブルとメソッドテーブル
JVMバイトコード
メソッド呼び出しと呼び出しスタック
オペランドスタックでの実行
ローカル変数
トップに戻る
javap
を使用したクラスファイルの逆コンパイルJavaにはjavap
が付属しています.class
を逆コンパイルするコマンドラインユーティリティ人間が読める形式にファイルします。 ScalaクラスファイルとJavaクラスファイルはどちらも同じJVMを対象としているため、javap
Scalaによってコンパイルされたクラスファイルを調べるために使用できます。
次のソースコードをコンパイルしましょう。
// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }
これをscalac RegularPolygon.scala
でコンパイルするRegularPolygon.class
を生成します。次にjavap RegularPolygon.class
を実行すると次のように表示されます。
$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
これはクラスファイルの非常に単純な内訳であり、クラスのパブリックメンバーの名前とタイプを単純に示しています。 -p
を追加するオプションにはプライベートメンバーが含まれます:
$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
これはまだ多くの情報ではありません。メソッドがJavaバイトコードでどのように実装されているかを確認するために、-c
を追加しましょう。オプション:
$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }
それはもう少し面白いです。ただし、実際に全体像を把握するには、-v
を使用する必要があります。または-verbose
javap -p -v RegularPolygon.class
のようなオプション:
ここで、最終的にクラスファイルに実際に何が含まれているかがわかります。これはどういう意味ですか?最も重要な部分のいくつかを見てみましょう。
C ++アプリケーションの開発サイクルには、コンパイルとリンケージの段階が含まれます。リンケージは実行時に発生するため、Javaの開発サイクルは明示的なリンケージステージをスキップします。クラスファイルは、このランタイムリンクをサポートする必要があります。つまり、ソースコードが任意のフィールドまたはメソッドを参照する場合、結果のバイトコードは関連する参照をシンボリック形式で保持し、アプリケーションがメモリにロードされ、ランタイムリンカーによって実際のアドレスを解決できるようになったら逆参照できるようにする必要があります。このシンボリックフォームには、次のものが含まれている必要があります。
クラスファイル形式の仕様には、ファイルの「 定数プール 、リンカが必要とするすべての参照の表。さまざまなタイプのエントリが含まれています。
// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...
各エントリの最初のバイトは、エントリのタイプを示す数値タグです。残りのバイトは、エントリの値に関する情報を提供します。バイト数とその解釈の規則は、最初のバイトで示されるタイプによって異なります。
たとえば、定数整数を使用するJavaクラス365
次のバイトコードを持つ定数プールエントリがある場合があります。
x03 00 00 01 6D
最初のバイトx03
は、エントリタイプCONSTANT_Integer
を識別します。これは、次の4バイトに整数の値が含まれていることをリンカに通知します。 (16進数の365はx16D
であることに注意してください)。これが定数プールの14番目のエントリである場合、javap -v
次のようにレンダリングされます:
#14 = Integer 365
多くの定数型は、定数プール内の他の場所にある、より「プリミティブな」定数型への参照で構成されています。たとえば、サンプルコードには次のステートメントが含まれています。
println( 'Calculating perimeter...' )
文字列定数を使用すると、定数プールに2つのエントリが生成されます。1つはタイプCONSTANT_String
のエントリです。 、およびタイプCONSTANT_Utf8
の別のエントリ。タイプConstant_UTF8
のエントリ文字列値の実際のUTF8表現が含まれます。タイプCONSTANT_String
のエントリCONSTANT_Utf8
への参照が含まれていますエントリ:
#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...
タイプUtf8
のエントリを参照する他のタイプの定数プールエントリがあるため、このような複雑さが必要です。タイプString
のエントリではありません。たとえば、クラス属性を参照すると、CONSTANT_Fieldref
が生成されます。タイプ。クラス名、属性名、および属性タイプへの一連の参照が含まれています。
#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I
定数プールの詳細については、を参照してください。 JVMドキュメント 。
クラスファイルには、 フィールドテーブル クラスで定義された各フィールド(つまり、属性)に関する情報が含まれています。これらは、フィールドの名前とタイプ、およびアクセス制御フラグとその他の関連データを説明する定数プールエントリへの参照です。
似たような メソッドテーブル クラスファイルに存在します。ただし、名前とタイプの情報に加えて、非抽象メソッドごとに、JVMによって実行される実際のバイトコード命令と、以下で説明するメソッドのスタックフレームによって使用されるデータ構造が含まれます。
JVMは、独自の内部命令セットを使用して、コンパイルされたコードを実行します。実行中javap
-c
でオプションは、コンパイルされたメソッドの実装を出力に含めます。 RegularPolygon.class
を調べるとこの方法でファイルすると、getPerimeter()
の次の出力が表示されます。方法:
public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn
実際のバイトコードは次のようになります。
xB2 00 17 x12 19 xB6 00 1D x27 ...
各命令は1バイトで始まります オペコード 特定の命令の形式に応じて、JVM命令を識別し、その後に操作対象の0個以上の命令オペランドを指定します。これらは通常、定数値、または定数プールへの参照のいずれかです。 javap
バイトコードを人間が読める形式に変換すると、次のように表示されます。
#23
などのポンド記号で表示されるオペランドは、定数プール内のエントリーへの参照です。ご覧のとおり、javap
また、出力に役立つコメントを生成し、プールから正確に参照されているものを識別します。
以下では、いくつかの一般的な手順について説明します。完全なJVM命令セットの詳細については、 ドキュメンテーション 。
各メソッド呼び出しは、ローカルで宣言された変数やメソッドに渡された引数などを含む、独自のコンテキストで実行できる必要があります。一緒に、これらは構成します スタックフレーム 。メソッドを呼び出すと、新しいフレームが作成され、その上に配置されます。 コールスタック 。メソッドが戻ると、現在のフレームが呼び出しスタックから削除されて破棄され、メソッドが呼び出される前に有効だったフレームが復元されます。
スタックフレームには、いくつかの異なる構造が含まれています。 2つの重要なものは オペランドスタック そしてその ローカル変数テーブル 、次に説明します。
多くのJVM命令は、フレームで動作します オペランドスタック 。これらの命令は、バイトコードで定数オペランドを明示的に指定するのではなく、オペランドスタックの最上位の値を入力として受け取ります。通常、これらの値はプロセスでスタックから削除されます。一部の命令では、スタックの最上位に新しい値も配置されます。このようにして、JVM命令を組み合わせて、複雑な操作を実行できます。たとえば、次の式です。
sideLength * this.numSides
getPerimeter()
で次のようにコンパイルされます方法:
8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul
dload_1
は、オブジェクト参照をローカル変数テーブル(次に説明)のスロット1からオペランドスタックにプッシュします。この場合、これはメソッド引数sideLength
.-次の命令aload_0
は、ローカル変数テーブルのスロット0にあるオブジェクト参照をオペランドスタックにプッシュします。実際には、これはほとんどの場合、現在のクラスであるthis
への参照です。invokevirtual #31
を実行する次の呼び出しnumSides()
のスタックが設定されます。 invokevirtual
最上位のオペランド(this
への参照)をスタックからポップして、メソッドを呼び出す必要のあるクラスを識別します。メソッドが戻ると、その結果はスタックにプッシュされます。numSides
)は整数形式です。別のdouble値を乗算するには、double浮動小数点形式に変換する必要があります。命令i2d
整数値をスタックからポップし、浮動小数点形式に変換して、スタックにプッシュバックします。this.numSides
の浮動小数点結果が含まれています。上にsideLength
の値が続きますメソッドに渡された引数。 dmul
これらの上位2つの値をスタックからポップし、それらに対して浮動小数点乗算を実行して、結果をスタックにプッシュします。メソッドが呼び出されると、そのスタックフレームの一部として新しいオペランドスタックが作成され、そこで操作が実行されます。ここでは用語に注意する必要があります。「スタック」という言葉は、 コールスタック 、メソッド実行または特定のフレームのコンテキストを提供するフレームのスタック オペランドスタック 、JVM命令が動作する。
各スタックフレームは、 ローカル変数 。これには通常、this
への参照が含まれますオブジェクト、メソッドが呼び出されたときに渡された引数、およびメソッド本体内で宣言されたローカル変数。実行中javap
-v
でオプションには、ローカル変数テーブルなど、各メソッドのスタックフレームの設定方法に関する情報が含まれます。
public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D
この例では、2つのローカル変数があります。スロット0の変数の名前はthis
で、タイプはRegularPolygon
です。これは、メソッド自体のクラスへの参照です。スロット1の変数の名前はsideLength
で、タイプはD
です。 (ダブルを示します)。これは、getPerimeter()
に渡される引数です。方法。
iload_1
、fstore_2
、aload [n]
などの命令は、オペランドスタックとローカル変数テーブルの間でさまざまなタイプのローカル変数を転送します。通常、表の最初の項目はthis
への参照であるため、命令aload_0
独自のクラスで動作するすべてのメソッドで一般的に見られます。
これで、JVMの基本についてのウォークスルーは終わりです。 メインの記事に戻るには、ここをクリックしてください。
; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmapScala言語は、その優れた組み合わせのおかげで、過去数年にわたって人気を博し続けています。 関数型およびオブジェクト指向のソフトウェア開発の原則 、および実績のあるJava仮想マシン(JVM)上でのその実装。
でも はしご Javaバイトコードにコンパイルされ、Java言語の認識されている欠点の多くを改善するように設計されています。完全な関数型プログラミングサポートを提供するScalaのコア構文には、Javaプログラマーが明示的に構築する必要のある多くの暗黙的な構造が含まれており、その中にはかなり複雑なものもあります。
Javaバイトコードにコンパイルする言語を作成するには、Java仮想マシンの内部動作を深く理解する必要があります。 Scalaの開発者が成し遂げたことを理解するには、内部を調べて、効率的で効果的なJVMバイトコードを生成するためにScalaのソースコードがコンパイラーによってどのように解釈されるかを調べる必要があります。
これらすべてがどのように実装されているかを見てみましょう。
この記事を読むには、Java仮想マシンのバイトコードの基本的な理解が必要です。完全な仮想マシンの仕様は、次のサイトから入手できます。 Oracleの公式ドキュメント 。この記事を理解するために仕様全体を読むことは重要ではないため、基本を簡単に紹介するために、記事の下部に短いガイドを用意しました。
JVMの基本に関するクラッシュコースを読むには、ここをクリックしてください。以下に示す例を再現するためにJavaバイトコードを逆アセンブルし、さらに調査を進めるには、ユーティリティが必要です。 Java Development Kitには、独自のコマンドラインユーティリティjavap
が用意されており、ここで使用します。 javap
の簡単なデモンストレーション作品が含まれています 下部のガイドで 。
そしてもちろん、例に沿ってフォローしたい読者には、Scalaコンパイラの実用的なインストールが必要です。この記事はを使用して書かれました スケール2.11.7 。 Scalaのバージョンが異なると、バイトコードがわずかに異なる場合があります。
Javaの規則では、パブリック属性のゲッターメソッドとセッターメソッドが常に提供されますが、Javaプログラマーは、それぞれのパターンが数十年にわたって変更されていないにもかかわらず、これらを自分で作成する必要があります。対照的に、Scalaはデフォルトのゲッターとセッターを提供します。
次の例を見てみましょう。
class Person(val name:String) { }
クラスの内部を見てみましょうPerson
。このファイルをscalac
でコンパイルすると、$ javap -p Person.class
が実行されます。私たちに与える:
Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }
Scalaクラスの各フィールドについて、フィールドとそのゲッターメソッドが生成されていることがわかります。フィールドはプライベートでファイナルですが、メソッドはパブリックです。
val
を置き換える場合var
でPerson
でソースを作成して再コンパイルしてから、フィールドのfinal
修飾子が削除され、setterメソッドも追加されます。
Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }
もしあればval
またはvar
がクラス本体内で定義されると、対応するプライベートフィールドとアクセサメソッドが作成され、インスタンスの作成時に適切に初期化されます。
クラスレベルのそのような実装に注意してくださいval
およびvar
フィールドは、いくつかの変数が中間値を格納するためにクラスレベルで使用され、プログラマーが直接アクセスしない場合、そのような各フィールドを初期化すると、クラスのフットプリントに1つから2つのメソッドが追加されることを意味します。 private
を追加するこのようなフィールドの修飾子は、対応するアクセサーが削除されることを意味するものではありません。彼らはただプライベートになります。
メソッドm()
があり、この関数への3つの異なるScalaスタイルの参照を作成するとします。
class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }
m
へのこれらの各参照はどうですか構築されましたか?いつm
いずれの場合も実行されますか?結果のバイトコードを見てみましょう。次の出力は、javap -v Person.class
の結果を示しています。 (多くの余分な出力を省略):
Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return
定数プールでは、メソッドm()
への参照がわかります。インデックス#30
に格納されます。コンストラクターコードでは、このメソッドが初期化中に2回呼び出され、命令invokevirtual #30
が使用されていることがわかります。最初にバイトオフセット11に表示され、次にオフセット19に表示されます。最初の呼び出しの後に命令putfield #22
が続きます。これは、このメソッドの結果を、インデックスm1
によって参照されるフィールド#22
に割り当てます。定数プールで。 2回目の呼び出しの後に同じパターンが続きます。今回は、m2
でインデックス付けされたフィールド#24
に値を割り当てます。定数プールで。
つまり、val
で定義された変数にメソッドを割り当てます。またはvar
のみを割り当てます 結果 その変数へのメソッドの。メソッドm1()
がわかりますおよびm2()
作成されるのは、これらの変数の単なるゲッターです。 var m2
の場合、セッターm2_$eq(int)
もわかります。が作成され、他のセッターと同じように動作し、フィールドの値を上書きします。
ただし、キーワードdef
を使用する別の結果が得られます。返すフィールド値をフェッチするのではなく、メソッドm3()
命令invokevirtual #30
も含まれています。つまり、このメソッドが呼び出されるたびに、m()
が呼び出され、このメソッドの結果が返されます。
したがって、ご覧のとおり、Scalaはクラスフィールドを操作する3つの方法を提供し、これらはキーワードval
、var
、およびdef
を介して簡単に指定できます。 Javaでは、必要なセッターとゲッターを明示的に実装する必要があり、そのような手動で記述されたボイラープレートコードは、表現力がはるかに低く、エラーが発生しやすくなります。
遅延値を宣言すると、より複雑なコードが生成されます。以前に定義したクラスに次のフィールドを追加したと仮定します。
lazy val m4 = m
実行中javap -p -v Person.class
これで、次のことが明らかになります。
Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn
この場合、フィールドの値m4
必要になるまで計算されません。特別なプライベートメソッドm4$lzycompute()
レイジー値を計算するために生成され、フィールドbitmap$0
その状態を追跡します。メソッドm4()
このフィールドの値が0であるかどうかをチェックし、m4
であることを示します。まだ初期化されていません。この場合、m4$lzycompute()
が呼び出され、m4
が入力されますそしてその値を返します。このプライベートメソッドは、bitmap$0
の値も設定します次回はm4()
が呼び出されると、初期化メソッドの呼び出しがスキップされ、代わりにm4
の値が返されます。
Scalaがここで生成するバイトコードは、スレッドセーフで効果的であるように設計されています。スレッドセーフにするために、レイジーコンピューティングメソッドはmonitorenter
/ monitorexit
を使用します。指示のペア。この同期のパフォーマンスオーバーヘッドは、遅延値の最初の読み取り時にのみ発生するため、この方法は引き続き有効です。
遅延値の状態を示すために必要なビットは1つだけです。したがって、レイジー値が32個以下の場合、1つのintフィールドでそれらすべてを追跡できます。ソースコードで複数の遅延値が定義されている場合、上記のバイトコードは、この目的のためにビットマスクを実装するようにコンパイラによって変更されます。
繰り返しになりますが、Scalaを使用すると、Javaで明示的に実装する必要がある特定の種類の動作を簡単に利用できるため、労力を節約し、タイプミスのリスクを減らすことができます。
それでは、次のScalaソースコードを見てみましょう。
class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }
Printer
クラスには、タイプoutput
の1つのフィールドString => Unit
があります。String
を受け取る関数です。タイプUnit
のオブジェクトを返します(Javaのvoid
に似ています)。 mainメソッドでは、これらのオブジェクトの1つを作成し、このフィールドを指定された文字列を出力する無名関数として割り当てます。
このコードをコンパイルすると、4つのクラスファイルが生成されます。
Hello.class
mainメソッドが単にHello$.main()
を呼び出すラッパークラスです。
public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return
隠されたHello$.class
mainメソッドの実際の実装が含まれています。そのバイトコードを確認するには、$
を正しくエスケープしていることを確認してください。コマンドシェルの規則に従って、特殊文字としての解釈を避けるために:
public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return
このメソッドはPrinter
を作成します。次に、匿名関数Hello$$anonfun$1
を含むs => println(s)
を作成します。 Printer
このオブジェクトでoutput
として初期化されますフィールド。次に、このフィールドはスタックにロードされ、オペランド'Hello'
で実行されます。
以下の無名関数クラスHello$$anonfun$1.class
を見てみましょう。 ScalaのFunction1
を拡張していることがわかります(AbstractFunction1
として)apply()
を実装する方法。実際には、2つのapply()
を作成しますメソッドは、一方が他方をラップし、一緒に型チェックを実行し(この場合、入力がString
であること)、無名関数を実行します(入力をprintln()
で出力します)。
public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn
Hello$.main()
を振り返って上記の方法では、オフセット21で、無名関数の実行がそのapply( Object )
の呼び出しによってトリガーされることがわかります。方法。
最後に、完全を期すために、Printer.class
のバイトコードを見てみましょう。
public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return
ここでの無名関数は他のval
と同じように扱われることがわかります。変数。クラスフィールドoutput
とゲッターoutput()
に格納されます創造された。唯一の違いは、この変数がScalaインターフェースを実装する必要があることですscala.Function1
(これはAbstractFunction1
が行います)。
したがって、このエレガントなScala機能のコストは、値として使用できる単一の無名関数を表現および実行するために作成された、基礎となるユーティリティクラスです。特定のアプリケーションにとってそれが何を意味するのかを理解するには、そのような機能の数とVM実装の詳細を考慮する必要があります。
Scalaの内部を理解する:この強力な言語がJVMバイトコードでどのように実装されているかを探ります。 つぶやきScalaの特性は、Javaのインターフェースに似ています。次の特性は、2つのメソッド署名を定義し、2番目のメソッドのデフォルトの実装を提供します。それがどのように実装されているか見てみましょう:
trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }
2つのエンティティが生成されます。両方のメソッドを宣言するインターフェイスであるSimilarity.class
と、デフォルトの実装を提供する合成クラスSimilarity$class.class
です。
public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); }
public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return
クラスがこの特性を実装し、メソッドisNotSimilar
を呼び出すと、Scalaコンパイラーはバイトコード命令invokestatic
を生成します。付随するクラスによって提供される静的メソッドを呼び出します。
複雑なポリモーフィズムと継承構造は、特性から作成される場合があります。たとえば、複数のトレイトと実装クラスがすべて、同じシグネチャを持つメソッドをオーバーライドして、super.methodName()
を呼び出す場合があります。次の特性に制御を渡すため。 Scalaコンパイラがそのような呼び出しに遭遇すると、次のようになります。
invokestatic
を生成します命令。したがって、トレイトの強力な概念が、大きなオーバーヘッドを引き起こさない方法でJVMレベルで実装されていることがわかります。また、Scalaプログラマーは、実行時にコストがかかりすぎることを心配せずにこの機能を楽しむことができます。
Scalaは、キーワードobject
を使用してシングルトンクラスの明示的な定義を提供します。次のシングルトンクラスについて考えてみましょう。
object Config { val home_dir = '/home/user' }
コンパイラは2つのクラスファイルを生成します。
Config.class
非常に単純なものです:
public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn
これは、合成のデコレータですConfig$
シングルトンの機能を組み込むクラス。 javap -p -c
でそのクラスを調べる次のバイトコードを生成します。
public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return
以下で構成されています。
MODULE$
。{}
(クラス初期化子とも呼ばれます)およびConfig$
の初期化に使用されるプライベートメソッドMODULE$
フィールドをデフォルト値に設定しますhome_dir
のゲッターメソッド。この場合、それは1つの方法にすぎません。シングルトンにさらに多くのフィールドがある場合、可変フィールドのセッターだけでなく、より多くのゲッターがあります。シングルトンは人気があり便利なデザインパターンです。 Java言語は、言語レベルでそれを指定する直接的な方法を提供しません。むしろ、Javaソースに実装するのは開発者の責任です。一方、Scalaは、object
を使用してシングルトンを明示的に宣言するための明確で便利な方法を提供します。キーワード。内部を見るとわかるように、手頃な価格で自然な方法で実装されています。
これで、Scalaがいくつかの暗黙的および関数型プログラミング機能を洗練されたJavaバイトコード構造にコンパイルする方法を見てきました。 Scalaの内部の仕組みを垣間見ることで、Scalaの力をより深く理解することができ、この強力な言語を最大限に活用するのに役立ちます。
また、自分たちで言語を探索するためのツールもあります。ケースクラス、カリー化、リスト内包表記など、この記事では取り上げていないScala構文の便利な機能がたくさんあります。これらの構造のScalaの実装を自分で調査して、次のレベルのScala忍者になる方法を学ぶことをお勧めします。
Javaコンパイラと同様に、Scalaコンパイラはソースコードを.class
に変換します。 Java仮想マシンによって実行されるJavaバイトコードを含むファイル。 2つの言語が内部でどのように異なるかを理解するには、両方が対象としているシステムを理解する必要があります。ここでは、Java仮想マシンアーキテクチャのいくつかの主要な要素、クラスファイル構造、およびアセンブラの基本について簡単に説明します。
このガイドでは、上記の記事に沿ってフォローできるようにするための最小限の内容のみを取り上げていることに注意してください。 JVMの多くの主要コンポーネントについてはここでは説明していませんが、完全な詳細は公式ドキュメントにあります。 ここに 。
javap
を使用したクラスファイルの逆コンパイル
コンスタントプール
フィールドテーブルとメソッドテーブル
JVMバイトコード
メソッド呼び出しと呼び出しスタック
オペランドスタックでの実行
ローカル変数
トップに戻る
javap
を使用したクラスファイルの逆コンパイルJavaにはjavap
が付属しています.class
を逆コンパイルするコマンドラインユーティリティ人間が読める形式にファイルします。 ScalaクラスファイルとJavaクラスファイルはどちらも同じJVMを対象としているため、javap
Scalaによってコンパイルされたクラスファイルを調べるために使用できます。
次のソースコードをコンパイルしましょう。
// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }
これをscalac RegularPolygon.scala
でコンパイルするRegularPolygon.class
を生成します。次にjavap RegularPolygon.class
を実行すると次のように表示されます。
$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
これはクラスファイルの非常に単純な内訳であり、クラスのパブリックメンバーの名前とタイプを単純に示しています。 -p
を追加するオプションにはプライベートメンバーが含まれます:
$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
これはまだ多くの情報ではありません。メソッドがJavaバイトコードでどのように実装されているかを確認するために、-c
を追加しましょう。オプション:
$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }
それはもう少し面白いです。ただし、実際に全体像を把握するには、-v
を使用する必要があります。または-verbose
javap -p -v RegularPolygon.class
のようなオプション:
ここで、最終的にクラスファイルに実際に何が含まれているかがわかります。これはどういう意味ですか?最も重要な部分のいくつかを見てみましょう。
C ++アプリケーションの開発サイクルには、コンパイルとリンケージの段階が含まれます。リンケージは実行時に発生するため、Javaの開発サイクルは明示的なリンケージステージをスキップします。クラスファイルは、このランタイムリンクをサポートする必要があります。つまり、ソースコードが任意のフィールドまたはメソッドを参照する場合、結果のバイトコードは関連する参照をシンボリック形式で保持し、アプリケーションがメモリにロードされ、ランタイムリンカーによって実際のアドレスを解決できるようになったら逆参照できるようにする必要があります。このシンボリックフォームには、次のものが含まれている必要があります。
クラスファイル形式の仕様には、ファイルの「 定数プール 、リンカが必要とするすべての参照の表。さまざまなタイプのエントリが含まれています。
// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...
各エントリの最初のバイトは、エントリのタイプを示す数値タグです。残りのバイトは、エントリの値に関する情報を提供します。バイト数とその解釈の規則は、最初のバイトで示されるタイプによって異なります。
たとえば、定数整数を使用するJavaクラス365
次のバイトコードを持つ定数プールエントリがある場合があります。
x03 00 00 01 6D
最初のバイトx03
は、エントリタイプCONSTANT_Integer
を識別します。これは、次の4バイトに整数の値が含まれていることをリンカに通知します。 (16進数の365はx16D
であることに注意してください)。これが定数プールの14番目のエントリである場合、javap -v
次のようにレンダリングされます:
#14 = Integer 365
多くの定数型は、定数プール内の他の場所にある、より「プリミティブな」定数型への参照で構成されています。たとえば、サンプルコードには次のステートメントが含まれています。
println( 'Calculating perimeter...' )
文字列定数を使用すると、定数プールに2つのエントリが生成されます。1つはタイプCONSTANT_String
のエントリです。 、およびタイプCONSTANT_Utf8
の別のエントリ。タイプConstant_UTF8
のエントリ文字列値の実際のUTF8表現が含まれます。タイプCONSTANT_String
のエントリCONSTANT_Utf8
への参照が含まれていますエントリ:
#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...
タイプUtf8
のエントリを参照する他のタイプの定数プールエントリがあるため、このような複雑さが必要です。タイプString
のエントリではありません。たとえば、クラス属性を参照すると、CONSTANT_Fieldref
が生成されます。タイプ。クラス名、属性名、および属性タイプへの一連の参照が含まれています。
#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I
定数プールの詳細については、を参照してください。 JVMドキュメント 。
クラスファイルには、 フィールドテーブル クラスで定義された各フィールド(つまり、属性)に関する情報が含まれています。これらは、フィールドの名前とタイプ、およびアクセス制御フラグとその他の関連データを説明する定数プールエントリへの参照です。
似たような メソッドテーブル クラスファイルに存在します。ただし、名前とタイプの情報に加えて、非抽象メソッドごとに、JVMによって実行される実際のバイトコード命令と、以下で説明するメソッドのスタックフレームによって使用されるデータ構造が含まれます。
JVMは、独自の内部命令セットを使用して、コンパイルされたコードを実行します。実行中javap
-c
でオプションは、コンパイルされたメソッドの実装を出力に含めます。 RegularPolygon.class
を調べるとこの方法でファイルすると、getPerimeter()
の次の出力が表示されます。方法:
public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn
実際のバイトコードは次のようになります。
xB2 00 17 x12 19 xB6 00 1D x27 ...
各命令は1バイトで始まります オペコード 特定の命令の形式に応じて、JVM命令を識別し、その後に操作対象の0個以上の命令オペランドを指定します。これらは通常、定数値、または定数プールへの参照のいずれかです。 javap
バイトコードを人間が読める形式に変換すると、次のように表示されます。
#23
などのポンド記号で表示されるオペランドは、定数プール内のエントリーへの参照です。ご覧のとおり、javap
また、出力に役立つコメントを生成し、プールから正確に参照されているものを識別します。
以下では、いくつかの一般的な手順について説明します。完全なJVM命令セットの詳細については、 ドキュメンテーション 。
各メソッド呼び出しは、ローカルで宣言された変数やメソッドに渡された引数などを含む、独自のコンテキストで実行できる必要があります。一緒に、これらは構成します スタックフレーム 。メソッドを呼び出すと、新しいフレームが作成され、その上に配置されます。 コールスタック 。メソッドが戻ると、現在のフレームが呼び出しスタックから削除されて破棄され、メソッドが呼び出される前に有効だったフレームが復元されます。
スタックフレームには、いくつかの異なる構造が含まれています。 2つの重要なものは オペランドスタック そしてその ローカル変数テーブル 、次に説明します。
多くのJVM命令は、フレームで動作します オペランドスタック 。これらの命令は、バイトコードで定数オペランドを明示的に指定するのではなく、オペランドスタックの最上位の値を入力として受け取ります。通常、これらの値はプロセスでスタックから削除されます。一部の命令では、スタックの最上位に新しい値も配置されます。このようにして、JVM命令を組み合わせて、複雑な操作を実行できます。たとえば、次の式です。
sideLength * this.numSides
getPerimeter()
で次のようにコンパイルされます方法:
8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul
dload_1
は、オブジェクト参照をローカル変数テーブル(次に説明)のスロット1からオペランドスタックにプッシュします。この場合、これはメソッド引数sideLength
.-次の命令aload_0
は、ローカル変数テーブルのスロット0にあるオブジェクト参照をオペランドスタックにプッシュします。実際には、これはほとんどの場合、現在のクラスであるthis
への参照です。invokevirtual #31
を実行する次の呼び出しnumSides()
のスタックが設定されます。 invokevirtual
最上位のオペランド(this
への参照)をスタックからポップして、メソッドを呼び出す必要のあるクラスを識別します。メソッドが戻ると、その結果はスタックにプッシュされます。numSides
)は整数形式です。別のdouble値を乗算するには、double浮動小数点形式に変換する必要があります。命令i2d
整数値をスタックからポップし、浮動小数点形式に変換して、スタックにプッシュバックします。this.numSides
の浮動小数点結果が含まれています。上にsideLength
の値が続きますメソッドに渡された引数。 dmul
これらの上位2つの値をスタックからポップし、それらに対して浮動小数点乗算を実行して、結果をスタックにプッシュします。メソッドが呼び出されると、そのスタックフレームの一部として新しいオペランドスタックが作成され、そこで操作が実行されます。ここでは用語に注意する必要があります。「スタック」という言葉は、 コールスタック 、メソッド実行または特定のフレームのコンテキストを提供するフレームのスタック オペランドスタック 、JVM命令が動作する。
各スタックフレームは、 ローカル変数 。これには通常、this
への参照が含まれますオブジェクト、メソッドが呼び出されたときに渡された引数、およびメソッド本体内で宣言されたローカル変数。実行中javap
-v
でオプションには、ローカル変数テーブルなど、各メソッドのスタックフレームの設定方法に関する情報が含まれます。
public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D
この例では、2つのローカル変数があります。スロット0の変数の名前はthis
で、タイプはRegularPolygon
です。これは、メソッド自体のクラスへの参照です。スロット1の変数の名前はsideLength
で、タイプはD
です。 (ダブルを示します)。これは、getPerimeter()
に渡される引数です。方法。
iload_1
、fstore_2
、aload [n]
などの命令は、オペランドスタックとローカル変数テーブルの間でさまざまなタイプのローカル変数を転送します。通常、表の最初の項目はthis
への参照であるため、命令aload_0
独自のクラスで動作するすべてのメソッドで一般的に見られます。
これで、JVMの基本についてのウォークスルーは終わりです。 メインの記事に戻るには、ここをクリックしてください。
:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmapScala言語は、その優れた組み合わせのおかげで、過去数年にわたって人気を博し続けています。 関数型およびオブジェクト指向のソフトウェア開発の原則 、および実績のあるJava仮想マシン(JVM)上でのその実装。
でも はしご Javaバイトコードにコンパイルされ、Java言語の認識されている欠点の多くを改善するように設計されています。完全な関数型プログラミングサポートを提供するScalaのコア構文には、Javaプログラマーが明示的に構築する必要のある多くの暗黙的な構造が含まれており、その中にはかなり複雑なものもあります。
Javaバイトコードにコンパイルする言語を作成するには、Java仮想マシンの内部動作を深く理解する必要があります。 Scalaの開発者が成し遂げたことを理解するには、内部を調べて、効率的で効果的なJVMバイトコードを生成するためにScalaのソースコードがコンパイラーによってどのように解釈されるかを調べる必要があります。
これらすべてがどのように実装されているかを見てみましょう。
この記事を読むには、Java仮想マシンのバイトコードの基本的な理解が必要です。完全な仮想マシンの仕様は、次のサイトから入手できます。 Oracleの公式ドキュメント 。この記事を理解するために仕様全体を読むことは重要ではないため、基本を簡単に紹介するために、記事の下部に短いガイドを用意しました。
JVMの基本に関するクラッシュコースを読むには、ここをクリックしてください。以下に示す例を再現するためにJavaバイトコードを逆アセンブルし、さらに調査を進めるには、ユーティリティが必要です。 Java Development Kitには、独自のコマンドラインユーティリティjavap
が用意されており、ここで使用します。 javap
の簡単なデモンストレーション作品が含まれています 下部のガイドで 。
そしてもちろん、例に沿ってフォローしたい読者には、Scalaコンパイラの実用的なインストールが必要です。この記事はを使用して書かれました スケール2.11.7 。 Scalaのバージョンが異なると、バイトコードがわずかに異なる場合があります。
Javaの規則では、パブリック属性のゲッターメソッドとセッターメソッドが常に提供されますが、Javaプログラマーは、それぞれのパターンが数十年にわたって変更されていないにもかかわらず、これらを自分で作成する必要があります。対照的に、Scalaはデフォルトのゲッターとセッターを提供します。
次の例を見てみましょう。
class Person(val name:String) { }
クラスの内部を見てみましょうPerson
。このファイルをscalac
でコンパイルすると、$ javap -p Person.class
が実行されます。私たちに与える:
Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }
Scalaクラスの各フィールドについて、フィールドとそのゲッターメソッドが生成されていることがわかります。フィールドはプライベートでファイナルですが、メソッドはパブリックです。
val
を置き換える場合var
でPerson
でソースを作成して再コンパイルしてから、フィールドのfinal
修飾子が削除され、setterメソッドも追加されます。
Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }
もしあればval
またはvar
がクラス本体内で定義されると、対応するプライベートフィールドとアクセサメソッドが作成され、インスタンスの作成時に適切に初期化されます。
クラスレベルのそのような実装に注意してくださいval
およびvar
フィールドは、いくつかの変数が中間値を格納するためにクラスレベルで使用され、プログラマーが直接アクセスしない場合、そのような各フィールドを初期化すると、クラスのフットプリントに1つから2つのメソッドが追加されることを意味します。 private
を追加するこのようなフィールドの修飾子は、対応するアクセサーが削除されることを意味するものではありません。彼らはただプライベートになります。
メソッドm()
があり、この関数への3つの異なるScalaスタイルの参照を作成するとします。
class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }
m
へのこれらの各参照はどうですか構築されましたか?いつm
いずれの場合も実行されますか?結果のバイトコードを見てみましょう。次の出力は、javap -v Person.class
の結果を示しています。 (多くの余分な出力を省略):
Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return
定数プールでは、メソッドm()
への参照がわかります。インデックス#30
に格納されます。コンストラクターコードでは、このメソッドが初期化中に2回呼び出され、命令invokevirtual #30
が使用されていることがわかります。最初にバイトオフセット11に表示され、次にオフセット19に表示されます。最初の呼び出しの後に命令putfield #22
が続きます。これは、このメソッドの結果を、インデックスm1
によって参照されるフィールド#22
に割り当てます。定数プールで。 2回目の呼び出しの後に同じパターンが続きます。今回は、m2
でインデックス付けされたフィールド#24
に値を割り当てます。定数プールで。
つまり、val
で定義された変数にメソッドを割り当てます。またはvar
のみを割り当てます 結果 その変数へのメソッドの。メソッドm1()
がわかりますおよびm2()
作成されるのは、これらの変数の単なるゲッターです。 var m2
の場合、セッターm2_$eq(int)
もわかります。が作成され、他のセッターと同じように動作し、フィールドの値を上書きします。
ただし、キーワードdef
を使用する別の結果が得られます。返すフィールド値をフェッチするのではなく、メソッドm3()
命令invokevirtual #30
も含まれています。つまり、このメソッドが呼び出されるたびに、m()
が呼び出され、このメソッドの結果が返されます。
したがって、ご覧のとおり、Scalaはクラスフィールドを操作する3つの方法を提供し、これらはキーワードval
、var
、およびdef
を介して簡単に指定できます。 Javaでは、必要なセッターとゲッターを明示的に実装する必要があり、そのような手動で記述されたボイラープレートコードは、表現力がはるかに低く、エラーが発生しやすくなります。
遅延値を宣言すると、より複雑なコードが生成されます。以前に定義したクラスに次のフィールドを追加したと仮定します。
lazy val m4 = m
実行中javap -p -v Person.class
これで、次のことが明らかになります。
Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn
この場合、フィールドの値m4
必要になるまで計算されません。特別なプライベートメソッドm4$lzycompute()
レイジー値を計算するために生成され、フィールドbitmap$0
その状態を追跡します。メソッドm4()
このフィールドの値が0であるかどうかをチェックし、m4
であることを示します。まだ初期化されていません。この場合、m4$lzycompute()
が呼び出され、m4
が入力されますそしてその値を返します。このプライベートメソッドは、bitmap$0
の値も設定します次回はm4()
が呼び出されると、初期化メソッドの呼び出しがスキップされ、代わりにm4
の値が返されます。
Scalaがここで生成するバイトコードは、スレッドセーフで効果的であるように設計されています。スレッドセーフにするために、レイジーコンピューティングメソッドはmonitorenter
/ monitorexit
を使用します。指示のペア。この同期のパフォーマンスオーバーヘッドは、遅延値の最初の読み取り時にのみ発生するため、この方法は引き続き有効です。
遅延値の状態を示すために必要なビットは1つだけです。したがって、レイジー値が32個以下の場合、1つのintフィールドでそれらすべてを追跡できます。ソースコードで複数の遅延値が定義されている場合、上記のバイトコードは、この目的のためにビットマスクを実装するようにコンパイラによって変更されます。
繰り返しになりますが、Scalaを使用すると、Javaで明示的に実装する必要がある特定の種類の動作を簡単に利用できるため、労力を節約し、タイプミスのリスクを減らすことができます。
それでは、次のScalaソースコードを見てみましょう。
class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }
Printer
クラスには、タイプoutput
の1つのフィールドString => Unit
があります。String
を受け取る関数です。タイプUnit
のオブジェクトを返します(Javaのvoid
に似ています)。 mainメソッドでは、これらのオブジェクトの1つを作成し、このフィールドを指定された文字列を出力する無名関数として割り当てます。
このコードをコンパイルすると、4つのクラスファイルが生成されます。
Hello.class
mainメソッドが単にHello$.main()
を呼び出すラッパークラスです。
public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return
隠されたHello$.class
mainメソッドの実際の実装が含まれています。そのバイトコードを確認するには、$
を正しくエスケープしていることを確認してください。コマンドシェルの規則に従って、特殊文字としての解釈を避けるために:
public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return
このメソッドはPrinter
を作成します。次に、匿名関数Hello$$anonfun$1
を含むs => println(s)
を作成します。 Printer
このオブジェクトでoutput
として初期化されますフィールド。次に、このフィールドはスタックにロードされ、オペランド'Hello'
で実行されます。
以下の無名関数クラスHello$$anonfun$1.class
を見てみましょう。 ScalaのFunction1
を拡張していることがわかります(AbstractFunction1
として)apply()
を実装する方法。実際には、2つのapply()
を作成しますメソッドは、一方が他方をラップし、一緒に型チェックを実行し(この場合、入力がString
であること)、無名関数を実行します(入力をprintln()
で出力します)。
public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn
Hello$.main()
を振り返って上記の方法では、オフセット21で、無名関数の実行がそのapply( Object )
の呼び出しによってトリガーされることがわかります。方法。
最後に、完全を期すために、Printer.class
のバイトコードを見てみましょう。
public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return
ここでの無名関数は他のval
と同じように扱われることがわかります。変数。クラスフィールドoutput
とゲッターoutput()
に格納されます創造された。唯一の違いは、この変数がScalaインターフェースを実装する必要があることですscala.Function1
(これはAbstractFunction1
が行います)。
したがって、このエレガントなScala機能のコストは、値として使用できる単一の無名関数を表現および実行するために作成された、基礎となるユーティリティクラスです。特定のアプリケーションにとってそれが何を意味するのかを理解するには、そのような機能の数とVM実装の詳細を考慮する必要があります。
Scalaの内部を理解する:この強力な言語がJVMバイトコードでどのように実装されているかを探ります。 つぶやきScalaの特性は、Javaのインターフェースに似ています。次の特性は、2つのメソッド署名を定義し、2番目のメソッドのデフォルトの実装を提供します。それがどのように実装されているか見てみましょう:
trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }
2つのエンティティが生成されます。両方のメソッドを宣言するインターフェイスであるSimilarity.class
と、デフォルトの実装を提供する合成クラスSimilarity$class.class
です。
public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); }
public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return
クラスがこの特性を実装し、メソッドisNotSimilar
を呼び出すと、Scalaコンパイラーはバイトコード命令invokestatic
を生成します。付随するクラスによって提供される静的メソッドを呼び出します。
複雑なポリモーフィズムと継承構造は、特性から作成される場合があります。たとえば、複数のトレイトと実装クラスがすべて、同じシグネチャを持つメソッドをオーバーライドして、super.methodName()
を呼び出す場合があります。次の特性に制御を渡すため。 Scalaコンパイラがそのような呼び出しに遭遇すると、次のようになります。
invokestatic
を生成します命令。したがって、トレイトの強力な概念が、大きなオーバーヘッドを引き起こさない方法でJVMレベルで実装されていることがわかります。また、Scalaプログラマーは、実行時にコストがかかりすぎることを心配せずにこの機能を楽しむことができます。
Scalaは、キーワードobject
を使用してシングルトンクラスの明示的な定義を提供します。次のシングルトンクラスについて考えてみましょう。
object Config { val home_dir = '/home/user' }
コンパイラは2つのクラスファイルを生成します。
Config.class
非常に単純なものです:
public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn
これは、合成のデコレータですConfig$
シングルトンの機能を組み込むクラス。 javap -p -c
でそのクラスを調べる次のバイトコードを生成します。
public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return
以下で構成されています。
MODULE$
。{}
(クラス初期化子とも呼ばれます)およびConfig$
の初期化に使用されるプライベートメソッドMODULE$
フィールドをデフォルト値に設定しますhome_dir
のゲッターメソッド。この場合、それは1つの方法にすぎません。シングルトンにさらに多くのフィールドがある場合、可変フィールドのセッターだけでなく、より多くのゲッターがあります。シングルトンは人気があり便利なデザインパターンです。 Java言語は、言語レベルでそれを指定する直接的な方法を提供しません。むしろ、Javaソースに実装するのは開発者の責任です。一方、Scalaは、object
を使用してシングルトンを明示的に宣言するための明確で便利な方法を提供します。キーワード。内部を見るとわかるように、手頃な価格で自然な方法で実装されています。
これで、Scalaがいくつかの暗黙的および関数型プログラミング機能を洗練されたJavaバイトコード構造にコンパイルする方法を見てきました。 Scalaの内部の仕組みを垣間見ることで、Scalaの力をより深く理解することができ、この強力な言語を最大限に活用するのに役立ちます。
また、自分たちで言語を探索するためのツールもあります。ケースクラス、カリー化、リスト内包表記など、この記事では取り上げていないScala構文の便利な機能がたくさんあります。これらの構造のScalaの実装を自分で調査して、次のレベルのScala忍者になる方法を学ぶことをお勧めします。
Javaコンパイラと同様に、Scalaコンパイラはソースコードを.class
に変換します。 Java仮想マシンによって実行されるJavaバイトコードを含むファイル。 2つの言語が内部でどのように異なるかを理解するには、両方が対象としているシステムを理解する必要があります。ここでは、Java仮想マシンアーキテクチャのいくつかの主要な要素、クラスファイル構造、およびアセンブラの基本について簡単に説明します。
このガイドでは、上記の記事に沿ってフォローできるようにするための最小限の内容のみを取り上げていることに注意してください。 JVMの多くの主要コンポーネントについてはここでは説明していませんが、完全な詳細は公式ドキュメントにあります。 ここに 。
javap
を使用したクラスファイルの逆コンパイル
コンスタントプール
フィールドテーブルとメソッドテーブル
JVMバイトコード
メソッド呼び出しと呼び出しスタック
オペランドスタックでの実行
ローカル変数
トップに戻る
javap
を使用したクラスファイルの逆コンパイルJavaにはjavap
が付属しています.class
を逆コンパイルするコマンドラインユーティリティ人間が読める形式にファイルします。 ScalaクラスファイルとJavaクラスファイルはどちらも同じJVMを対象としているため、javap
Scalaによってコンパイルされたクラスファイルを調べるために使用できます。
次のソースコードをコンパイルしましょう。
// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }
これをscalac RegularPolygon.scala
でコンパイルするRegularPolygon.class
を生成します。次にjavap RegularPolygon.class
を実行すると次のように表示されます。
$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
これはクラスファイルの非常に単純な内訳であり、クラスのパブリックメンバーの名前とタイプを単純に示しています。 -p
を追加するオプションにはプライベートメンバーが含まれます:
$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
これはまだ多くの情報ではありません。メソッドがJavaバイトコードでどのように実装されているかを確認するために、-c
を追加しましょう。オプション:
$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }
それはもう少し面白いです。ただし、実際に全体像を把握するには、-v
を使用する必要があります。または-verbose
javap -p -v RegularPolygon.class
のようなオプション:
ここで、最終的にクラスファイルに実際に何が含まれているかがわかります。これはどういう意味ですか?最も重要な部分のいくつかを見てみましょう。
C ++アプリケーションの開発サイクルには、コンパイルとリンケージの段階が含まれます。リンケージは実行時に発生するため、Javaの開発サイクルは明示的なリンケージステージをスキップします。クラスファイルは、このランタイムリンクをサポートする必要があります。つまり、ソースコードが任意のフィールドまたはメソッドを参照する場合、結果のバイトコードは関連する参照をシンボリック形式で保持し、アプリケーションがメモリにロードされ、ランタイムリンカーによって実際のアドレスを解決できるようになったら逆参照できるようにする必要があります。このシンボリックフォームには、次のものが含まれている必要があります。
クラスファイル形式の仕様には、ファイルの「 定数プール 、リンカが必要とするすべての参照の表。さまざまなタイプのエントリが含まれています。
// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...
各エントリの最初のバイトは、エントリのタイプを示す数値タグです。残りのバイトは、エントリの値に関する情報を提供します。バイト数とその解釈の規則は、最初のバイトで示されるタイプによって異なります。
たとえば、定数整数を使用するJavaクラス365
次のバイトコードを持つ定数プールエントリがある場合があります。
x03 00 00 01 6D
最初のバイトx03
は、エントリタイプCONSTANT_Integer
を識別します。これは、次の4バイトに整数の値が含まれていることをリンカに通知します。 (16進数の365はx16D
であることに注意してください)。これが定数プールの14番目のエントリである場合、javap -v
次のようにレンダリングされます:
#14 = Integer 365
多くの定数型は、定数プール内の他の場所にある、より「プリミティブな」定数型への参照で構成されています。たとえば、サンプルコードには次のステートメントが含まれています。
println( 'Calculating perimeter...' )
文字列定数を使用すると、定数プールに2つのエントリが生成されます。1つはタイプCONSTANT_String
のエントリです。 、およびタイプCONSTANT_Utf8
の別のエントリ。タイプConstant_UTF8
のエントリ文字列値の実際のUTF8表現が含まれます。タイプCONSTANT_String
のエントリCONSTANT_Utf8
への参照が含まれていますエントリ:
#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...
タイプUtf8
のエントリを参照する他のタイプの定数プールエントリがあるため、このような複雑さが必要です。タイプString
のエントリではありません。たとえば、クラス属性を参照すると、CONSTANT_Fieldref
が生成されます。タイプ。クラス名、属性名、および属性タイプへの一連の参照が含まれています。
#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I
定数プールの詳細については、を参照してください。 JVMドキュメント 。
クラスファイルには、 フィールドテーブル クラスで定義された各フィールド(つまり、属性)に関する情報が含まれています。これらは、フィールドの名前とタイプ、およびアクセス制御フラグとその他の関連データを説明する定数プールエントリへの参照です。
似たような メソッドテーブル クラスファイルに存在します。ただし、名前とタイプの情報に加えて、非抽象メソッドごとに、JVMによって実行される実際のバイトコード命令と、以下で説明するメソッドのスタックフレームによって使用されるデータ構造が含まれます。
JVMは、独自の内部命令セットを使用して、コンパイルされたコードを実行します。実行中javap
-c
でオプションは、コンパイルされたメソッドの実装を出力に含めます。 RegularPolygon.class
を調べるとこの方法でファイルすると、getPerimeter()
の次の出力が表示されます。方法:
public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn
実際のバイトコードは次のようになります。
xB2 00 17 x12 19 xB6 00 1D x27 ...
各命令は1バイトで始まります オペコード 特定の命令の形式に応じて、JVM命令を識別し、その後に操作対象の0個以上の命令オペランドを指定します。これらは通常、定数値、または定数プールへの参照のいずれかです。 javap
バイトコードを人間が読める形式に変換すると、次のように表示されます。
#23
などのポンド記号で表示されるオペランドは、定数プール内のエントリーへの参照です。ご覧のとおり、javap
また、出力に役立つコメントを生成し、プールから正確に参照されているものを識別します。
以下では、いくつかの一般的な手順について説明します。完全なJVM命令セットの詳細については、 ドキュメンテーション 。
各メソッド呼び出しは、ローカルで宣言された変数やメソッドに渡された引数などを含む、独自のコンテキストで実行できる必要があります。一緒に、これらは構成します スタックフレーム 。メソッドを呼び出すと、新しいフレームが作成され、その上に配置されます。 コールスタック 。メソッドが戻ると、現在のフレームが呼び出しスタックから削除されて破棄され、メソッドが呼び出される前に有効だったフレームが復元されます。
スタックフレームには、いくつかの異なる構造が含まれています。 2つの重要なものは オペランドスタック そしてその ローカル変数テーブル 、次に説明します。
多くのJVM命令は、フレームで動作します オペランドスタック 。これらの命令は、バイトコードで定数オペランドを明示的に指定するのではなく、オペランドスタックの最上位の値を入力として受け取ります。通常、これらの値はプロセスでスタックから削除されます。一部の命令では、スタックの最上位に新しい値も配置されます。このようにして、JVM命令を組み合わせて、複雑な操作を実行できます。たとえば、次の式です。
sideLength * this.numSides
getPerimeter()
で次のようにコンパイルされます方法:
8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul
dload_1
は、オブジェクト参照をローカル変数テーブル(次に説明)のスロット1からオペランドスタックにプッシュします。この場合、これはメソッド引数sideLength
.-次の命令aload_0
は、ローカル変数テーブルのスロット0にあるオブジェクト参照をオペランドスタックにプッシュします。実際には、これはほとんどの場合、現在のクラスであるthis
への参照です。invokevirtual #31
を実行する次の呼び出しnumSides()
のスタックが設定されます。 invokevirtual
最上位のオペランド(this
への参照)をスタックからポップして、メソッドを呼び出す必要のあるクラスを識別します。メソッドが戻ると、その結果はスタックにプッシュされます。numSides
)は整数形式です。別のdouble値を乗算するには、double浮動小数点形式に変換する必要があります。命令i2d
整数値をスタックからポップし、浮動小数点形式に変換して、スタックにプッシュバックします。this.numSides
の浮動小数点結果が含まれています。上にsideLength
の値が続きますメソッドに渡された引数。 dmul
これらの上位2つの値をスタックからポップし、それらに対して浮動小数点乗算を実行して、結果をスタックにプッシュします。メソッドが呼び出されると、そのスタックフレームの一部として新しいオペランドスタックが作成され、そこで操作が実行されます。ここでは用語に注意する必要があります。「スタック」という言葉は、 コールスタック 、メソッド実行または特定のフレームのコンテキストを提供するフレームのスタック オペランドスタック 、JVM命令が動作する。
各スタックフレームは、 ローカル変数 。これには通常、this
への参照が含まれますオブジェクト、メソッドが呼び出されたときに渡された引数、およびメソッド本体内で宣言されたローカル変数。実行中javap
-v
でオプションには、ローカル変数テーブルなど、各メソッドのスタックフレームの設定方法に関する情報が含まれます。
public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D
この例では、2つのローカル変数があります。スロット0の変数の名前はthis
で、タイプはRegularPolygon
です。これは、メソッド自体のクラスへの参照です。スロット1の変数の名前はsideLength
で、タイプはD
です。 (ダブルを示します)。これは、getPerimeter()
に渡される引数です。方法。
iload_1
、fstore_2
、aload [n]
などの命令は、オペランドスタックとローカル変数テーブルの間でさまざまなタイプのローカル変数を転送します。通常、表の最初の項目はthis
への参照であるため、命令aload_0
独自のクラスで動作するすべてのメソッドで一般的に見られます。
これで、JVMの基本についてのウォークスルーは終わりです。 メインの記事に戻るには、ここをクリックしてください。
:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmapScala言語は、その優れた組み合わせのおかげで、過去数年にわたって人気を博し続けています。 関数型およびオブジェクト指向のソフトウェア開発の原則 、および実績のあるJava仮想マシン(JVM)上でのその実装。
でも はしご Javaバイトコードにコンパイルされ、Java言語の認識されている欠点の多くを改善するように設計されています。完全な関数型プログラミングサポートを提供するScalaのコア構文には、Javaプログラマーが明示的に構築する必要のある多くの暗黙的な構造が含まれており、その中にはかなり複雑なものもあります。
Javaバイトコードにコンパイルする言語を作成するには、Java仮想マシンの内部動作を深く理解する必要があります。 Scalaの開発者が成し遂げたことを理解するには、内部を調べて、効率的で効果的なJVMバイトコードを生成するためにScalaのソースコードがコンパイラーによってどのように解釈されるかを調べる必要があります。
これらすべてがどのように実装されているかを見てみましょう。
この記事を読むには、Java仮想マシンのバイトコードの基本的な理解が必要です。完全な仮想マシンの仕様は、次のサイトから入手できます。 Oracleの公式ドキュメント 。この記事を理解するために仕様全体を読むことは重要ではないため、基本を簡単に紹介するために、記事の下部に短いガイドを用意しました。
JVMの基本に関するクラッシュコースを読むには、ここをクリックしてください。以下に示す例を再現するためにJavaバイトコードを逆アセンブルし、さらに調査を進めるには、ユーティリティが必要です。 Java Development Kitには、独自のコマンドラインユーティリティjavap
が用意されており、ここで使用します。 javap
の簡単なデモンストレーション作品が含まれています 下部のガイドで 。
そしてもちろん、例に沿ってフォローしたい読者には、Scalaコンパイラの実用的なインストールが必要です。この記事はを使用して書かれました スケール2.11.7 。 Scalaのバージョンが異なると、バイトコードがわずかに異なる場合があります。
Javaの規則では、パブリック属性のゲッターメソッドとセッターメソッドが常に提供されますが、Javaプログラマーは、それぞれのパターンが数十年にわたって変更されていないにもかかわらず、これらを自分で作成する必要があります。対照的に、Scalaはデフォルトのゲッターとセッターを提供します。
次の例を見てみましょう。
class Person(val name:String) { }
クラスの内部を見てみましょうPerson
。このファイルをscalac
でコンパイルすると、$ javap -p Person.class
が実行されます。私たちに与える:
Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }
Scalaクラスの各フィールドについて、フィールドとそのゲッターメソッドが生成されていることがわかります。フィールドはプライベートでファイナルですが、メソッドはパブリックです。
val
を置き換える場合var
でPerson
でソースを作成して再コンパイルしてから、フィールドのfinal
修飾子が削除され、setterメソッドも追加されます。
Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }
もしあればval
またはvar
がクラス本体内で定義されると、対応するプライベートフィールドとアクセサメソッドが作成され、インスタンスの作成時に適切に初期化されます。
クラスレベルのそのような実装に注意してくださいval
およびvar
フィールドは、いくつかの変数が中間値を格納するためにクラスレベルで使用され、プログラマーが直接アクセスしない場合、そのような各フィールドを初期化すると、クラスのフットプリントに1つから2つのメソッドが追加されることを意味します。 private
を追加するこのようなフィールドの修飾子は、対応するアクセサーが削除されることを意味するものではありません。彼らはただプライベートになります。
メソッドm()
があり、この関数への3つの異なるScalaスタイルの参照を作成するとします。
class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }
m
へのこれらの各参照はどうですか構築されましたか?いつm
いずれの場合も実行されますか?結果のバイトコードを見てみましょう。次の出力は、javap -v Person.class
の結果を示しています。 (多くの余分な出力を省略):
Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return
定数プールでは、メソッドm()
への参照がわかります。インデックス#30
に格納されます。コンストラクターコードでは、このメソッドが初期化中に2回呼び出され、命令invokevirtual #30
が使用されていることがわかります。最初にバイトオフセット11に表示され、次にオフセット19に表示されます。最初の呼び出しの後に命令putfield #22
が続きます。これは、このメソッドの結果を、インデックスm1
によって参照されるフィールド#22
に割り当てます。定数プールで。 2回目の呼び出しの後に同じパターンが続きます。今回は、m2
でインデックス付けされたフィールド#24
に値を割り当てます。定数プールで。
つまり、val
で定義された変数にメソッドを割り当てます。またはvar
のみを割り当てます 結果 その変数へのメソッドの。メソッドm1()
がわかりますおよびm2()
作成されるのは、これらの変数の単なるゲッターです。 var m2
の場合、セッターm2_$eq(int)
もわかります。が作成され、他のセッターと同じように動作し、フィールドの値を上書きします。
ただし、キーワードdef
を使用する別の結果が得られます。返すフィールド値をフェッチするのではなく、メソッドm3()
命令invokevirtual #30
も含まれています。つまり、このメソッドが呼び出されるたびに、m()
が呼び出され、このメソッドの結果が返されます。
したがって、ご覧のとおり、Scalaはクラスフィールドを操作する3つの方法を提供し、これらはキーワードval
、var
、およびdef
を介して簡単に指定できます。 Javaでは、必要なセッターとゲッターを明示的に実装する必要があり、そのような手動で記述されたボイラープレートコードは、表現力がはるかに低く、エラーが発生しやすくなります。
遅延値を宣言すると、より複雑なコードが生成されます。以前に定義したクラスに次のフィールドを追加したと仮定します。
lazy val m4 = m
実行中javap -p -v Person.class
これで、次のことが明らかになります。
Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn
この場合、フィールドの値m4
必要になるまで計算されません。特別なプライベートメソッドm4$lzycompute()
レイジー値を計算するために生成され、フィールドbitmap$0
その状態を追跡します。メソッドm4()
このフィールドの値が0であるかどうかをチェックし、m4
であることを示します。まだ初期化されていません。この場合、m4$lzycompute()
が呼び出され、m4
が入力されますそしてその値を返します。このプライベートメソッドは、bitmap$0
の値も設定します次回はm4()
が呼び出されると、初期化メソッドの呼び出しがスキップされ、代わりにm4
の値が返されます。
Scalaがここで生成するバイトコードは、スレッドセーフで効果的であるように設計されています。スレッドセーフにするために、レイジーコンピューティングメソッドはmonitorenter
/ monitorexit
を使用します。指示のペア。この同期のパフォーマンスオーバーヘッドは、遅延値の最初の読み取り時にのみ発生するため、この方法は引き続き有効です。
遅延値の状態を示すために必要なビットは1つだけです。したがって、レイジー値が32個以下の場合、1つのintフィールドでそれらすべてを追跡できます。ソースコードで複数の遅延値が定義されている場合、上記のバイトコードは、この目的のためにビットマスクを実装するようにコンパイラによって変更されます。
繰り返しになりますが、Scalaを使用すると、Javaで明示的に実装する必要がある特定の種類の動作を簡単に利用できるため、労力を節約し、タイプミスのリスクを減らすことができます。
それでは、次のScalaソースコードを見てみましょう。
class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }
Printer
クラスには、タイプoutput
の1つのフィールドString => Unit
があります。String
を受け取る関数です。タイプUnit
のオブジェクトを返します(Javaのvoid
に似ています)。 mainメソッドでは、これらのオブジェクトの1つを作成し、このフィールドを指定された文字列を出力する無名関数として割り当てます。
このコードをコンパイルすると、4つのクラスファイルが生成されます。
Hello.class
mainメソッドが単にHello$.main()
を呼び出すラッパークラスです。
public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return
隠されたHello$.class
mainメソッドの実際の実装が含まれています。そのバイトコードを確認するには、$
を正しくエスケープしていることを確認してください。コマンドシェルの規則に従って、特殊文字としての解釈を避けるために:
public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return
このメソッドはPrinter
を作成します。次に、匿名関数Hello$$anonfun$1
を含むs => println(s)
を作成します。 Printer
このオブジェクトでoutput
として初期化されますフィールド。次に、このフィールドはスタックにロードされ、オペランド'Hello'
で実行されます。
以下の無名関数クラスHello$$anonfun$1.class
を見てみましょう。 ScalaのFunction1
を拡張していることがわかります(AbstractFunction1
として)apply()
を実装する方法。実際には、2つのapply()
を作成しますメソッドは、一方が他方をラップし、一緒に型チェックを実行し(この場合、入力がString
であること)、無名関数を実行します(入力をprintln()
で出力します)。
public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn
Hello$.main()
を振り返って上記の方法では、オフセット21で、無名関数の実行がそのapply( Object )
の呼び出しによってトリガーされることがわかります。方法。
最後に、完全を期すために、Printer.class
のバイトコードを見てみましょう。
public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return
ここでの無名関数は他のval
と同じように扱われることがわかります。変数。クラスフィールドoutput
とゲッターoutput()
に格納されます創造された。唯一の違いは、この変数がScalaインターフェースを実装する必要があることですscala.Function1
(これはAbstractFunction1
が行います)。
したがって、このエレガントなScala機能のコストは、値として使用できる単一の無名関数を表現および実行するために作成された、基礎となるユーティリティクラスです。特定のアプリケーションにとってそれが何を意味するのかを理解するには、そのような機能の数とVM実装の詳細を考慮する必要があります。
Scalaの内部を理解する:この強力な言語がJVMバイトコードでどのように実装されているかを探ります。 つぶやきScalaの特性は、Javaのインターフェースに似ています。次の特性は、2つのメソッド署名を定義し、2番目のメソッドのデフォルトの実装を提供します。それがどのように実装されているか見てみましょう:
trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }
2つのエンティティが生成されます。両方のメソッドを宣言するインターフェイスであるSimilarity.class
と、デフォルトの実装を提供する合成クラスSimilarity$class.class
です。
public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); }
public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return
クラスがこの特性を実装し、メソッドisNotSimilar
を呼び出すと、Scalaコンパイラーはバイトコード命令invokestatic
を生成します。付随するクラスによって提供される静的メソッドを呼び出します。
複雑なポリモーフィズムと継承構造は、特性から作成される場合があります。たとえば、複数のトレイトと実装クラスがすべて、同じシグネチャを持つメソッドをオーバーライドして、super.methodName()
を呼び出す場合があります。次の特性に制御を渡すため。 Scalaコンパイラがそのような呼び出しに遭遇すると、次のようになります。
invokestatic
を生成します命令。したがって、トレイトの強力な概念が、大きなオーバーヘッドを引き起こさない方法でJVMレベルで実装されていることがわかります。また、Scalaプログラマーは、実行時にコストがかかりすぎることを心配せずにこの機能を楽しむことができます。
Scalaは、キーワードobject
を使用してシングルトンクラスの明示的な定義を提供します。次のシングルトンクラスについて考えてみましょう。
object Config { val home_dir = '/home/user' }
コンパイラは2つのクラスファイルを生成します。
Config.class
非常に単純なものです:
public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn
これは、合成のデコレータですConfig$
シングルトンの機能を組み込むクラス。 javap -p -c
でそのクラスを調べる次のバイトコードを生成します。
public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return
以下で構成されています。
MODULE$
。{}
(クラス初期化子とも呼ばれます)およびConfig$
の初期化に使用されるプライベートメソッドMODULE$
フィールドをデフォルト値に設定しますhome_dir
のゲッターメソッド。この場合、それは1つの方法にすぎません。シングルトンにさらに多くのフィールドがある場合、可変フィールドのセッターだけでなく、より多くのゲッターがあります。シングルトンは人気があり便利なデザインパターンです。 Java言語は、言語レベルでそれを指定する直接的な方法を提供しません。むしろ、Javaソースに実装するのは開発者の責任です。一方、Scalaは、object
を使用してシングルトンを明示的に宣言するための明確で便利な方法を提供します。キーワード。内部を見るとわかるように、手頃な価格で自然な方法で実装されています。
これで、Scalaがいくつかの暗黙的および関数型プログラミング機能を洗練されたJavaバイトコード構造にコンパイルする方法を見てきました。 Scalaの内部の仕組みを垣間見ることで、Scalaの力をより深く理解することができ、この強力な言語を最大限に活用するのに役立ちます。
また、自分たちで言語を探索するためのツールもあります。ケースクラス、カリー化、リスト内包表記など、この記事では取り上げていないScala構文の便利な機能がたくさんあります。これらの構造のScalaの実装を自分で調査して、次のレベルのScala忍者になる方法を学ぶことをお勧めします。
Javaコンパイラと同様に、Scalaコンパイラはソースコードを.class
に変換します。 Java仮想マシンによって実行されるJavaバイトコードを含むファイル。 2つの言語が内部でどのように異なるかを理解するには、両方が対象としているシステムを理解する必要があります。ここでは、Java仮想マシンアーキテクチャのいくつかの主要な要素、クラスファイル構造、およびアセンブラの基本について簡単に説明します。
このガイドでは、上記の記事に沿ってフォローできるようにするための最小限の内容のみを取り上げていることに注意してください。 JVMの多くの主要コンポーネントについてはここでは説明していませんが、完全な詳細は公式ドキュメントにあります。 ここに 。
javap
を使用したクラスファイルの逆コンパイル
コンスタントプール
フィールドテーブルとメソッドテーブル
JVMバイトコード
メソッド呼び出しと呼び出しスタック
オペランドスタックでの実行
ローカル変数
トップに戻る
javap
を使用したクラスファイルの逆コンパイルJavaにはjavap
が付属しています.class
を逆コンパイルするコマンドラインユーティリティ人間が読める形式にファイルします。 ScalaクラスファイルとJavaクラスファイルはどちらも同じJVMを対象としているため、javap
Scalaによってコンパイルされたクラスファイルを調べるために使用できます。
次のソースコードをコンパイルしましょう。
// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }
これをscalac RegularPolygon.scala
でコンパイルするRegularPolygon.class
を生成します。次にjavap RegularPolygon.class
を実行すると次のように表示されます。
$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
これはクラスファイルの非常に単純な内訳であり、クラスのパブリックメンバーの名前とタイプを単純に示しています。 -p
を追加するオプションにはプライベートメンバーが含まれます:
$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
これはまだ多くの情報ではありません。メソッドがJavaバイトコードでどのように実装されているかを確認するために、-c
を追加しましょう。オプション:
$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }
それはもう少し面白いです。ただし、実際に全体像を把握するには、-v
を使用する必要があります。または-verbose
javap -p -v RegularPolygon.class
のようなオプション:
ここで、最終的にクラスファイルに実際に何が含まれているかがわかります。これはどういう意味ですか?最も重要な部分のいくつかを見てみましょう。
C ++アプリケーションの開発サイクルには、コンパイルとリンケージの段階が含まれます。リンケージは実行時に発生するため、Javaの開発サイクルは明示的なリンケージステージをスキップします。クラスファイルは、このランタイムリンクをサポートする必要があります。つまり、ソースコードが任意のフィールドまたはメソッドを参照する場合、結果のバイトコードは関連する参照をシンボリック形式で保持し、アプリケーションがメモリにロードされ、ランタイムリンカーによって実際のアドレスを解決できるようになったら逆参照できるようにする必要があります。このシンボリックフォームには、次のものが含まれている必要があります。
クラスファイル形式の仕様には、ファイルの「 定数プール 、リンカが必要とするすべての参照の表。さまざまなタイプのエントリが含まれています。
// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...
各エントリの最初のバイトは、エントリのタイプを示す数値タグです。残りのバイトは、エントリの値に関する情報を提供します。バイト数とその解釈の規則は、最初のバイトで示されるタイプによって異なります。
たとえば、定数整数を使用するJavaクラス365
次のバイトコードを持つ定数プールエントリがある場合があります。
x03 00 00 01 6D
最初のバイトx03
は、エントリタイプCONSTANT_Integer
を識別します。これは、次の4バイトに整数の値が含まれていることをリンカに通知します。 (16進数の365はx16D
であることに注意してください)。これが定数プールの14番目のエントリである場合、javap -v
次のようにレンダリングされます:
#14 = Integer 365
多くの定数型は、定数プール内の他の場所にある、より「プリミティブな」定数型への参照で構成されています。たとえば、サンプルコードには次のステートメントが含まれています。
println( 'Calculating perimeter...' )
文字列定数を使用すると、定数プールに2つのエントリが生成されます。1つはタイプCONSTANT_String
のエントリです。 、およびタイプCONSTANT_Utf8
の別のエントリ。タイプConstant_UTF8
のエントリ文字列値の実際のUTF8表現が含まれます。タイプCONSTANT_String
のエントリCONSTANT_Utf8
への参照が含まれていますエントリ:
#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...
タイプUtf8
のエントリを参照する他のタイプの定数プールエントリがあるため、このような複雑さが必要です。タイプString
のエントリではありません。たとえば、クラス属性を参照すると、CONSTANT_Fieldref
が生成されます。タイプ。クラス名、属性名、および属性タイプへの一連の参照が含まれています。
#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I
定数プールの詳細については、を参照してください。 JVMドキュメント 。
クラスファイルには、 フィールドテーブル クラスで定義された各フィールド(つまり、属性)に関する情報が含まれています。これらは、フィールドの名前とタイプ、およびアクセス制御フラグとその他の関連データを説明する定数プールエントリへの参照です。
似たような メソッドテーブル クラスファイルに存在します。ただし、名前とタイプの情報に加えて、非抽象メソッドごとに、JVMによって実行される実際のバイトコード命令と、以下で説明するメソッドのスタックフレームによって使用されるデータ構造が含まれます。
JVMは、独自の内部命令セットを使用して、コンパイルされたコードを実行します。実行中javap
-c
でオプションは、コンパイルされたメソッドの実装を出力に含めます。 RegularPolygon.class
を調べるとこの方法でファイルすると、getPerimeter()
の次の出力が表示されます。方法:
public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn
実際のバイトコードは次のようになります。
xB2 00 17 x12 19 xB6 00 1D x27 ...
各命令は1バイトで始まります オペコード 特定の命令の形式に応じて、JVM命令を識別し、その後に操作対象の0個以上の命令オペランドを指定します。これらは通常、定数値、または定数プールへの参照のいずれかです。 javap
バイトコードを人間が読める形式に変換すると、次のように表示されます。
#23
などのポンド記号で表示されるオペランドは、定数プール内のエントリーへの参照です。ご覧のとおり、javap
また、出力に役立つコメントを生成し、プールから正確に参照されているものを識別します。
以下では、いくつかの一般的な手順について説明します。完全なJVM命令セットの詳細については、 ドキュメンテーション 。
各メソッド呼び出しは、ローカルで宣言された変数やメソッドに渡された引数などを含む、独自のコンテキストで実行できる必要があります。一緒に、これらは構成します スタックフレーム 。メソッドを呼び出すと、新しいフレームが作成され、その上に配置されます。 コールスタック 。メソッドが戻ると、現在のフレームが呼び出しスタックから削除されて破棄され、メソッドが呼び出される前に有効だったフレームが復元されます。
スタックフレームには、いくつかの異なる構造が含まれています。 2つの重要なものは オペランドスタック そしてその ローカル変数テーブル 、次に説明します。
多くのJVM命令は、フレームで動作します オペランドスタック 。これらの命令は、バイトコードで定数オペランドを明示的に指定するのではなく、オペランドスタックの最上位の値を入力として受け取ります。通常、これらの値はプロセスでスタックから削除されます。一部の命令では、スタックの最上位に新しい値も配置されます。このようにして、JVM命令を組み合わせて、複雑な操作を実行できます。たとえば、次の式です。
sideLength * this.numSides
getPerimeter()
で次のようにコンパイルされます方法:
8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul
dload_1
は、オブジェクト参照をローカル変数テーブル(次に説明)のスロット1からオペランドスタックにプッシュします。この場合、これはメソッド引数sideLength
.-次の命令aload_0
は、ローカル変数テーブルのスロット0にあるオブジェクト参照をオペランドスタックにプッシュします。実際には、これはほとんどの場合、現在のクラスであるthis
への参照です。invokevirtual #31
を実行する次の呼び出しnumSides()
のスタックが設定されます。 invokevirtual
最上位のオペランド(this
への参照)をスタックからポップして、メソッドを呼び出す必要のあるクラスを識別します。メソッドが戻ると、その結果はスタックにプッシュされます。numSides
)は整数形式です。別のdouble値を乗算するには、double浮動小数点形式に変換する必要があります。命令i2d
整数値をスタックからポップし、浮動小数点形式に変換して、スタックにプッシュバックします。this.numSides
の浮動小数点結果が含まれています。上にsideLength
の値が続きますメソッドに渡された引数。 dmul
これらの上位2つの値をスタックからポップし、それらに対して浮動小数点乗算を実行して、結果をスタックにプッシュします。メソッドが呼び出されると、そのスタックフレームの一部として新しいオペランドスタックが作成され、そこで操作が実行されます。ここでは用語に注意する必要があります。「スタック」という言葉は、 コールスタック 、メソッド実行または特定のフレームのコンテキストを提供するフレームのスタック オペランドスタック 、JVM命令が動作する。
各スタックフレームは、 ローカル変数 。これには通常、this
への参照が含まれますオブジェクト、メソッドが呼び出されたときに渡された引数、およびメソッド本体内で宣言されたローカル変数。実行中javap
-v
でオプションには、ローカル変数テーブルなど、各メソッドのスタックフレームの設定方法に関する情報が含まれます。
public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D
この例では、2つのローカル変数があります。スロット0の変数の名前はthis
で、タイプはRegularPolygon
です。これは、メソッド自体のクラスへの参照です。スロット1の変数の名前はsideLength
で、タイプはD
です。 (ダブルを示します)。これは、getPerimeter()
に渡される引数です。方法。
iload_1
、fstore_2
、aload [n]
などの命令は、オペランドスタックとローカル変数テーブルの間でさまざまなタイプのローカル変数を転送します。通常、表の最初の項目はthis
への参照であるため、命令aload_0
独自のクラスで動作するすべてのメソッドで一般的に見られます。
これで、JVMの基本についてのウォークスルーは終わりです。 メインの記事に戻るには、ここをクリックしてください。
:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturnこの場合、フィールドの値 Scala言語は、その優れた組み合わせのおかげで、過去数年にわたって人気を博し続けています。 関数型およびオブジェクト指向のソフトウェア開発の原則 、および実績のあるJava仮想マシン(JVM)上でのその実装。 でも はしご Javaバイトコードにコンパイルされ、Java言語の認識されている欠点の多くを改善するように設計されています。完全な関数型プログラミングサポートを提供するScalaのコア構文には、Javaプログラマーが明示的に構築する必要のある多くの暗黙的な構造が含まれており、その中にはかなり複雑なものもあります。 Javaバイトコードにコンパイルする言語を作成するには、Java仮想マシンの内部動作を深く理解する必要があります。 Scalaの開発者が成し遂げたことを理解するには、内部を調べて、効率的で効果的なJVMバイトコードを生成するためにScalaのソースコードがコンパイラーによってどのように解釈されるかを調べる必要があります。 これらすべてがどのように実装されているかを見てみましょう。 この記事を読むには、Java仮想マシンのバイトコードの基本的な理解が必要です。完全な仮想マシンの仕様は、次のサイトから入手できます。 Oracleの公式ドキュメント 。この記事を理解するために仕様全体を読むことは重要ではないため、基本を簡単に紹介するために、記事の下部に短いガイドを用意しました。 以下に示す例を再現するためにJavaバイトコードを逆アセンブルし、さらに調査を進めるには、ユーティリティが必要です。 Java Development Kitには、独自のコマンドラインユーティリティ そしてもちろん、例に沿ってフォローしたい読者には、Scalaコンパイラの実用的なインストールが必要です。この記事はを使用して書かれました スケール2.11.7 。 Scalaのバージョンが異なると、バイトコードがわずかに異なる場合があります。 Javaの規則では、パブリック属性のゲッターメソッドとセッターメソッドが常に提供されますが、Javaプログラマーは、それぞれのパターンが数十年にわたって変更されていないにもかかわらず、これらを自分で作成する必要があります。対照的に、Scalaはデフォルトのゲッターとセッターを提供します。 次の例を見てみましょう。 クラスの内部を見てみましょう Scalaクラスの各フィールドについて、フィールドとそのゲッターメソッドが生成されていることがわかります。フィールドはプライベートでファイナルですが、メソッドはパブリックです。 もしあれば クラスレベルのそのような実装に注意してください メソッド 定数プールでは、メソッド つまり、 ただし、キーワード したがって、ご覧のとおり、Scalaはクラスフィールドを操作する3つの方法を提供し、これらはキーワード 遅延値を宣言すると、より複雑なコードが生成されます。以前に定義したクラスに次のフィールドを追加したと仮定します。 実行中 この場合、フィールドの値 Scalaがここで生成するバイトコードは、スレッドセーフで効果的であるように設計されています。スレッドセーフにするために、レイジーコンピューティングメソッドは 遅延値の状態を示すために必要なビットは1つだけです。したがって、レイジー値が32個以下の場合、1つのintフィールドでそれらすべてを追跡できます。ソースコードで複数の遅延値が定義されている場合、上記のバイトコードは、この目的のためにビットマスクを実装するようにコンパイラによって変更されます。 繰り返しになりますが、Scalaを使用すると、Javaで明示的に実装する必要がある特定の種類の動作を簡単に利用できるため、労力を節約し、タイプミスのリスクを減らすことができます。 それでは、次のScalaソースコードを見てみましょう。 このコードをコンパイルすると、4つのクラスファイルが生成されます。 隠された このメソッドは 以下の無名関数クラス 最後に、完全を期すために、 ここでの無名関数は他の したがって、このエレガントなScala機能のコストは、値として使用できる単一の無名関数を表現および実行するために作成された、基礎となるユーティリティクラスです。特定のアプリケーションにとってそれが何を意味するのかを理解するには、そのような機能の数とVM実装の詳細を考慮する必要があります。 Scalaの特性は、Javaのインターフェースに似ています。次の特性は、2つのメソッド署名を定義し、2番目のメソッドのデフォルトの実装を提供します。それがどのように実装されているか見てみましょう: 2つのエンティティが生成されます。両方のメソッドを宣言するインターフェイスである クラスがこの特性を実装し、メソッド 複雑なポリモーフィズムと継承構造は、特性から作成される場合があります。たとえば、複数のトレイトと実装クラスがすべて、同じシグネチャを持つメソッドをオーバーライドして、 したがって、トレイトの強力な概念が、大きなオーバーヘッドを引き起こさない方法でJVMレベルで実装されていることがわかります。また、Scalaプログラマーは、実行時にコストがかかりすぎることを心配せずにこの機能を楽しむことができます。 Scalaは、キーワード コンパイラは2つのクラスファイルを生成します。 これは、合成のデコレータです 以下で構成されています。 シングルトンは人気があり便利なデザインパターンです。 Java言語は、言語レベルでそれを指定する直接的な方法を提供しません。むしろ、Javaソースに実装するのは開発者の責任です。一方、Scalaは、 これで、Scalaがいくつかの暗黙的および関数型プログラミング機能を洗練されたJavaバイトコード構造にコンパイルする方法を見てきました。 Scalaの内部の仕組みを垣間見ることで、Scalaの力をより深く理解することができ、この強力な言語を最大限に活用するのに役立ちます。 また、自分たちで言語を探索するためのツールもあります。ケースクラス、カリー化、リスト内包表記など、この記事では取り上げていないScala構文の便利な機能がたくさんあります。これらの構造のScalaの実装を自分で調査して、次のレベルのScala忍者になる方法を学ぶことをお勧めします。 Javaコンパイラと同様に、Scalaコンパイラはソースコードを このガイドでは、上記の記事に沿ってフォローできるようにするための最小限の内容のみを取り上げていることに注意してください。 JVMの多くの主要コンポーネントについてはここでは説明していませんが、完全な詳細は公式ドキュメントにあります。 ここに 。 Javaには 次のソースコードをコンパイルしましょう。 これを これはクラスファイルの非常に単純な内訳であり、クラスのパブリックメンバーの名前とタイプを単純に示しています。 これはまだ多くの情報ではありません。メソッドがJavaバイトコードでどのように実装されているかを確認するために、 それはもう少し面白いです。ただし、実際に全体像を把握するには、 ここで、最終的にクラスファイルに実際に何が含まれているかがわかります。これはどういう意味ですか?最も重要な部分のいくつかを見てみましょう。 C ++アプリケーションの開発サイクルには、コンパイルとリンケージの段階が含まれます。リンケージは実行時に発生するため、Javaの開発サイクルは明示的なリンケージステージをスキップします。クラスファイルは、このランタイムリンクをサポートする必要があります。つまり、ソースコードが任意のフィールドまたはメソッドを参照する場合、結果のバイトコードは関連する参照をシンボリック形式で保持し、アプリケーションがメモリにロードされ、ランタイムリンカーによって実際のアドレスを解決できるようになったら逆参照できるようにする必要があります。このシンボリックフォームには、次のものが含まれている必要があります。 クラスファイル形式の仕様には、ファイルの「 定数プール 、リンカが必要とするすべての参照の表。さまざまなタイプのエントリが含まれています。 各エントリの最初のバイトは、エントリのタイプを示す数値タグです。残りのバイトは、エントリの値に関する情報を提供します。バイト数とその解釈の規則は、最初のバイトで示されるタイプによって異なります。 たとえば、定数整数を使用するJavaクラス 最初のバイト 多くの定数型は、定数プール内の他の場所にある、より「プリミティブな」定数型への参照で構成されています。たとえば、サンプルコードには次のステートメントが含まれています。 文字列定数を使用すると、定数プールに2つのエントリが生成されます。1つはタイプ タイプ 定数プールの詳細については、を参照してください。 JVMドキュメント 。 クラスファイルには、 フィールドテーブル クラスで定義された各フィールド(つまり、属性)に関する情報が含まれています。これらは、フィールドの名前とタイプ、およびアクセス制御フラグとその他の関連データを説明する定数プールエントリへの参照です。 似たような メソッドテーブル クラスファイルに存在します。ただし、名前とタイプの情報に加えて、非抽象メソッドごとに、JVMによって実行される実際のバイトコード命令と、以下で説明するメソッドのスタックフレームによって使用されるデータ構造が含まれます。 JVMは、独自の内部命令セットを使用して、コンパイルされたコードを実行します。実行中 実際のバイトコードは次のようになります。 各命令は1バイトで始まります オペコード 特定の命令の形式に応じて、JVM命令を識別し、その後に操作対象の0個以上の命令オペランドを指定します。これらは通常、定数値、または定数プールへの参照のいずれかです。 以下では、いくつかの一般的な手順について説明します。完全なJVM命令セットの詳細については、 ドキュメンテーション 。 各メソッド呼び出しは、ローカルで宣言された変数やメソッドに渡された引数などを含む、独自のコンテキストで実行できる必要があります。一緒に、これらは構成します スタックフレーム 。メソッドを呼び出すと、新しいフレームが作成され、その上に配置されます。 コールスタック 。メソッドが戻ると、現在のフレームが呼び出しスタックから削除されて破棄され、メソッドが呼び出される前に有効だったフレームが復元されます。 スタックフレームには、いくつかの異なる構造が含まれています。 2つの重要なものは オペランドスタック そしてその ローカル変数テーブル 、次に説明します。 多くのJVM命令は、フレームで動作します オペランドスタック 。これらの命令は、バイトコードで定数オペランドを明示的に指定するのではなく、オペランドスタックの最上位の値を入力として受け取ります。通常、これらの値はプロセスでスタックから削除されます。一部の命令では、スタックの最上位に新しい値も配置されます。このようにして、JVM命令を組み合わせて、複雑な操作を実行できます。たとえば、次の式です。 メソッドが呼び出されると、そのスタックフレームの一部として新しいオペランドスタックが作成され、そこで操作が実行されます。ここでは用語に注意する必要があります。「スタック」という言葉は、 コールスタック 、メソッド実行または特定のフレームのコンテキストを提供するフレームのスタック オペランドスタック 、JVM命令が動作する。 各スタックフレームは、 ローカル変数 。これには通常、 この例では、2つのローカル変数があります。スロット0の変数の名前は これで、JVMの基本についてのウォークスルーは終わりです。 メインの記事に戻るには、ここをクリックしてください。 Scala言語は、その優れた組み合わせのおかげで、過去数年にわたって人気を博し続けています。 関数型およびオブジェクト指向のソフトウェア開発の原則 、および実績のあるJava仮想マシン(JVM)上でのその実装。 でも はしご Javaバイトコードにコンパイルされ、Java言語の認識されている欠点の多くを改善するように設計されています。完全な関数型プログラミングサポートを提供するScalaのコア構文には、Javaプログラマーが明示的に構築する必要のある多くの暗黙的な構造が含まれており、その中にはかなり複雑なものもあります。 Javaバイトコードにコンパイルする言語を作成するには、Java仮想マシンの内部動作を深く理解する必要があります。 Scalaの開発者が成し遂げたことを理解するには、内部を調べて、効率的で効果的なJVMバイトコードを生成するためにScalaのソースコードがコンパイラーによってどのように解釈されるかを調べる必要があります。 これらすべてがどのように実装されているかを見てみましょう。 この記事を読むには、Java仮想マシンのバイトコードの基本的な理解が必要です。完全な仮想マシンの仕様は、次のサイトから入手できます。 Oracleの公式ドキュメント 。この記事を理解するために仕様全体を読むことは重要ではないため、基本を簡単に紹介するために、記事の下部に短いガイドを用意しました。 以下に示す例を再現するためにJavaバイトコードを逆アセンブルし、さらに調査を進めるには、ユーティリティが必要です。 Java Development Kitには、独自のコマンドラインユーティリティ そしてもちろん、例に沿ってフォローしたい読者には、Scalaコンパイラの実用的なインストールが必要です。この記事はを使用して書かれました スケール2.11.7 。 Scalaのバージョンが異なると、バイトコードがわずかに異なる場合があります。 Javaの規則では、パブリック属性のゲッターメソッドとセッターメソッドが常に提供されますが、Javaプログラマーは、それぞれのパターンが数十年にわたって変更されていないにもかかわらず、これらを自分で作成する必要があります。対照的に、Scalaはデフォルトのゲッターとセッターを提供します。 次の例を見てみましょう。 クラスの内部を見てみましょう Scalaクラスの各フィールドについて、フィールドとそのゲッターメソッドが生成されていることがわかります。フィールドはプライベートでファイナルですが、メソッドはパブリックです。 もしあれば クラスレベルのそのような実装に注意してください メソッド 定数プールでは、メソッド つまり、 ただし、キーワード したがって、ご覧のとおり、Scalaはクラスフィールドを操作する3つの方法を提供し、これらはキーワード 遅延値を宣言すると、より複雑なコードが生成されます。以前に定義したクラスに次のフィールドを追加したと仮定します。 実行中 この場合、フィールドの値 Scalaがここで生成するバイトコードは、スレッドセーフで効果的であるように設計されています。スレッドセーフにするために、レイジーコンピューティングメソッドは 遅延値の状態を示すために必要なビットは1つだけです。したがって、レイジー値が32個以下の場合、1つのintフィールドでそれらすべてを追跡できます。ソースコードで複数の遅延値が定義されている場合、上記のバイトコードは、この目的のためにビットマスクを実装するようにコンパイラによって変更されます。 繰り返しになりますが、Scalaを使用すると、Javaで明示的に実装する必要がある特定の種類の動作を簡単に利用できるため、労力を節約し、タイプミスのリスクを減らすことができます。 それでは、次のScalaソースコードを見てみましょう。 このコードをコンパイルすると、4つのクラスファイルが生成されます。 隠された このメソッドは 以下の無名関数クラス 最後に、完全を期すために、 ここでの無名関数は他の したがって、このエレガントなScala機能のコストは、値として使用できる単一の無名関数を表現および実行するために作成された、基礎となるユーティリティクラスです。特定のアプリケーションにとってそれが何を意味するのかを理解するには、そのような機能の数とVM実装の詳細を考慮する必要があります。 Scalaの特性は、Javaのインターフェースに似ています。次の特性は、2つのメソッド署名を定義し、2番目のメソッドのデフォルトの実装を提供します。それがどのように実装されているか見てみましょう: 2つのエンティティが生成されます。両方のメソッドを宣言するインターフェイスである クラスがこの特性を実装し、メソッド 複雑なポリモーフィズムと継承構造は、特性から作成される場合があります。たとえば、複数のトレイトと実装クラスがすべて、同じシグネチャを持つメソッドをオーバーライドして、 したがって、トレイトの強力な概念が、大きなオーバーヘッドを引き起こさない方法でJVMレベルで実装されていることがわかります。また、Scalaプログラマーは、実行時にコストがかかりすぎることを心配せずにこの機能を楽しむことができます。 Scalaは、キーワード コンパイラは2つのクラスファイルを生成します。 これは、合成のデコレータです 以下で構成されています。 シングルトンは人気があり便利なデザインパターンです。 Java言語は、言語レベルでそれを指定する直接的な方法を提供しません。むしろ、Javaソースに実装するのは開発者の責任です。一方、Scalaは、 これで、Scalaがいくつかの暗黙的および関数型プログラミング機能を洗練されたJavaバイトコード構造にコンパイルする方法を見てきました。 Scalaの内部の仕組みを垣間見ることで、Scalaの力をより深く理解することができ、この強力な言語を最大限に活用するのに役立ちます。 また、自分たちで言語を探索するためのツールもあります。ケースクラス、カリー化、リスト内包表記など、この記事では取り上げていないScala構文の便利な機能がたくさんあります。これらの構造のScalaの実装を自分で調査して、次のレベルのScala忍者になる方法を学ぶことをお勧めします。 Javaコンパイラと同様に、Scalaコンパイラはソースコードを このガイドでは、上記の記事に沿ってフォローできるようにするための最小限の内容のみを取り上げていることに注意してください。 JVMの多くの主要コンポーネントについてはここでは説明していませんが、完全な詳細は公式ドキュメントにあります。 ここに 。 Javaには 次のソースコードをコンパイルしましょう。 これを これはクラスファイルの非常に単純な内訳であり、クラスのパブリックメンバーの名前とタイプを単純に示しています。 これはまだ多くの情報ではありません。メソッドがJavaバイトコードでどのように実装されているかを確認するために、 それはもう少し面白いです。ただし、実際に全体像を把握するには、 ここで、最終的にクラスファイルに実際に何が含まれているかがわかります。これはどういう意味ですか?最も重要な部分のいくつかを見てみましょう。 C ++アプリケーションの開発サイクルには、コンパイルとリンケージの段階が含まれます。リンケージは実行時に発生するため、Javaの開発サイクルは明示的なリンケージステージをスキップします。クラスファイルは、このランタイムリンクをサポートする必要があります。つまり、ソースコードが任意のフィールドまたはメソッドを参照する場合、結果のバイトコードは関連する参照をシンボリック形式で保持し、アプリケーションがメモリにロードされ、ランタイムリンカーによって実際のアドレスを解決できるようになったら逆参照できるようにする必要があります。このシンボリックフォームには、次のものが含まれている必要があります。 クラスファイル形式の仕様には、ファイルの「 定数プール 、リンカが必要とするすべての参照の表。さまざまなタイプのエントリが含まれています。 各エントリの最初のバイトは、エントリのタイプを示す数値タグです。残りのバイトは、エントリの値に関する情報を提供します。バイト数とその解釈の規則は、最初のバイトで示されるタイプによって異なります。 たとえば、定数整数を使用するJavaクラス 最初のバイト 多くの定数型は、定数プール内の他の場所にある、より「プリミティブな」定数型への参照で構成されています。たとえば、サンプルコードには次のステートメントが含まれています。 文字列定数を使用すると、定数プールに2つのエントリが生成されます。1つはタイプ タイプ 定数プールの詳細については、を参照してください。 JVMドキュメント 。 クラスファイルには、 フィールドテーブル クラスで定義された各フィールド(つまり、属性)に関する情報が含まれています。これらは、フィールドの名前とタイプ、およびアクセス制御フラグとその他の関連データを説明する定数プールエントリへの参照です。 似たような メソッドテーブル クラスファイルに存在します。ただし、名前とタイプの情報に加えて、非抽象メソッドごとに、JVMによって実行される実際のバイトコード命令と、以下で説明するメソッドのスタックフレームによって使用されるデータ構造が含まれます。 JVMは、独自の内部命令セットを使用して、コンパイルされたコードを実行します。実行中 実際のバイトコードは次のようになります。 各命令は1バイトで始まります オペコード 特定の命令の形式に応じて、JVM命令を識別し、その後に操作対象の0個以上の命令オペランドを指定します。これらは通常、定数値、または定数プールへの参照のいずれかです。 以下では、いくつかの一般的な手順について説明します。完全なJVM命令セットの詳細については、 ドキュメンテーション 。 各メソッド呼び出しは、ローカルで宣言された変数やメソッドに渡された引数などを含む、独自のコンテキストで実行できる必要があります。一緒に、これらは構成します スタックフレーム 。メソッドを呼び出すと、新しいフレームが作成され、その上に配置されます。 コールスタック 。メソッドが戻ると、現在のフレームが呼び出しスタックから削除されて破棄され、メソッドが呼び出される前に有効だったフレームが復元されます。 スタックフレームには、いくつかの異なる構造が含まれています。 2つの重要なものは オペランドスタック そしてその ローカル変数テーブル 、次に説明します。 多くのJVM命令は、フレームで動作します オペランドスタック 。これらの命令は、バイトコードで定数オペランドを明示的に指定するのではなく、オペランドスタックの最上位の値を入力として受け取ります。通常、これらの値はプロセスでスタックから削除されます。一部の命令では、スタックの最上位に新しい値も配置されます。このようにして、JVM命令を組み合わせて、複雑な操作を実行できます。たとえば、次の式です。 メソッドが呼び出されると、そのスタックフレームの一部として新しいオペランドスタックが作成され、そこで操作が実行されます。ここでは用語に注意する必要があります。「スタック」という言葉は、 コールスタック 、メソッド実行または特定のフレームのコンテキストを提供するフレームのスタック オペランドスタック 、JVM命令が動作する。 各スタックフレームは、 ローカル変数 。これには通常、 この例では、2つのローカル変数があります。スロット0の変数の名前は これで、JVMの基本についてのウォークスルーは終わりです。 メインの記事に戻るには、ここをクリックしてください。 m4
必要になるまで計算されません。特別なプライベートメソッドm4$lzycompute()
レイジー値を計算するために生成され、フィールドbitmap
その状態を追跡します。メソッドScalaJVMバイトコードで手を汚す
前提条件
javap
が用意されており、ここで使用します。 javap
の簡単なデモンストレーション作品が含まれています 下部のガイドで 。デフォルトのゲッターとセッター
class Person(val name:String) { }
Person
。このファイルをscalac
でコンパイルすると、$ javap -p Person.class
が実行されます。私たちに与える:Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }
val
を置き換える場合var
でPerson
でソースを作成して再コンパイルしてから、フィールドのfinal
修飾子が削除され、setterメソッドも追加されます。Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }
val
またはvar
がクラス本体内で定義されると、対応するプライベートフィールドとアクセサメソッドが作成され、インスタンスの作成時に適切に初期化されます。val
およびvar
フィールドは、いくつかの変数が中間値を格納するためにクラスレベルで使用され、プログラマーが直接アクセスしない場合、そのような各フィールドを初期化すると、クラスのフットプリントに1つから2つのメソッドが追加されることを意味します。 private
を追加するこのようなフィールドの修飾子は、対応するアクセサーが削除されることを意味するものではありません。彼らはただプライベートになります。変数と関数の定義
m()
があり、この関数への3つの異なるScalaスタイルの参照を作成するとします。class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }
m
へのこれらの各参照はどうですか構築されましたか?いつm
いずれの場合も実行されますか?結果のバイトコードを見てみましょう。次の出力は、javap -v Person.class
の結果を示しています。 (多くの余分な出力を省略):Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return
m()
への参照がわかります。インデックス#30
に格納されます。コンストラクターコードでは、このメソッドが初期化中に2回呼び出され、命令invokevirtual #30
が使用されていることがわかります。最初にバイトオフセット11に表示され、次にオフセット19に表示されます。最初の呼び出しの後に命令putfield #22
が続きます。これは、このメソッドの結果を、インデックスm1
によって参照されるフィールド#22
に割り当てます。定数プールで。 2回目の呼び出しの後に同じパターンが続きます。今回は、m2
でインデックス付けされたフィールド#24
に値を割り当てます。定数プールで。val
で定義された変数にメソッドを割り当てます。またはvar
のみを割り当てます 結果 その変数へのメソッドの。メソッドm1()
がわかりますおよびm2()
作成されるのは、これらの変数の単なるゲッターです。 var m2
の場合、セッターm2_$eq(int)
もわかります。が作成され、他のセッターと同じように動作し、フィールドの値を上書きします。def
を使用する別の結果が得られます。返すフィールド値をフェッチするのではなく、メソッドm3()
命令invokevirtual #30
も含まれています。つまり、このメソッドが呼び出されるたびに、m()
が呼び出され、このメソッドの結果が返されます。val
、var
、およびdef
を介して簡単に指定できます。 Javaでは、必要なセッターとゲッターを明示的に実装する必要があり、そのような手動で記述されたボイラープレートコードは、表現力がはるかに低く、エラーが発生しやすくなります。怠惰な価値観
lazy val m4 = m
javap -p -v Person.class
これで、次のことが明らかになります。Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn
m4
必要になるまで計算されません。特別なプライベートメソッドm4$lzycompute()
レイジー値を計算するために生成され、フィールドbitmap$0
その状態を追跡します。メソッドm4()
このフィールドの値が0であるかどうかをチェックし、m4
であることを示します。まだ初期化されていません。この場合、m4$lzycompute()
が呼び出され、m4
が入力されますそしてその値を返します。このプライベートメソッドは、bitmap$0
の値も設定します次回はm4()
が呼び出されると、初期化メソッドの呼び出しがスキップされ、代わりにm4
の値が返されます。monitorenter
/ monitorexit
を使用します。指示のペア。この同期のパフォーマンスオーバーヘッドは、遅延値の最初の読み取り時にのみ発生するため、この方法は引き続き有効です。価値として機能する
class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }
Printer
クラスには、タイプoutput
の1つのフィールドString => Unit
があります。String
を受け取る関数です。タイプUnit
のオブジェクトを返します(Javaのvoid
に似ています)。 mainメソッドでは、これらのオブジェクトの1つを作成し、このフィールドを指定された文字列を出力する無名関数として割り当てます。Hello.class
mainメソッドが単にHello$.main()
を呼び出すラッパークラスです。public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return
Hello$.class
mainメソッドの実際の実装が含まれています。そのバイトコードを確認するには、$
を正しくエスケープしていることを確認してください。コマンドシェルの規則に従って、特殊文字としての解釈を避けるために:public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return
Printer
を作成します。次に、匿名関数Hello$$anonfun$1
を含むs => println(s)
を作成します。 Printer
このオブジェクトでoutput
として初期化されますフィールド。次に、このフィールドはスタックにロードされ、オペランド'Hello'
で実行されます。Hello$$anonfun$1.class
を見てみましょう。 ScalaのFunction1
を拡張していることがわかります(AbstractFunction1
として)apply()
を実装する方法。実際には、2つのapply()
を作成しますメソッドは、一方が他方をラップし、一緒に型チェックを実行し(この場合、入力がString
であること)、無名関数を実行します(入力をprintln()
で出力します)。public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn
Hello$.main()
を振り返って上記の方法では、オフセット21で、無名関数の実行がそのapply( Object )
の呼び出しによってトリガーされることがわかります。方法。Printer.class
のバイトコードを見てみましょう。public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return
val
と同じように扱われることがわかります。変数。クラスフィールドoutput
とゲッターoutput()
に格納されます創造された。唯一の違いは、この変数がScalaインターフェースを実装する必要があることですscala.Function1
(これはAbstractFunction1
が行います)。Scalaの特性
trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }
Similarity.class
と、デフォルトの実装を提供する合成クラスSimilarity$class.class
です。public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); }
public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return
isNotSimilar
を呼び出すと、Scalaコンパイラーはバイトコード命令invokestatic
を生成します。付随するクラスによって提供される静的メソッドを呼び出します。super.methodName()
を呼び出す場合があります。次の特性に制御を渡すため。 Scalaコンパイラがそのような呼び出しに遭遇すると、次のようになります。
invokestatic
を生成します命令。シングルトン
object
を使用してシングルトンクラスの明示的な定義を提供します。次のシングルトンクラスについて考えてみましょう。object Config { val home_dir = '/home/user' }
Config.class
非常に単純なものです:public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn
Config$
シングルトンの機能を組み込むクラス。 javap -p -c
でそのクラスを調べる次のバイトコードを生成します。public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return
MODULE$
。{}
(クラス初期化子とも呼ばれます)およびConfig$
の初期化に使用されるプライベートメソッドMODULE$
フィールドをデフォルト値に設定しますhome_dir
のゲッターメソッド。この場合、それは1つの方法にすぎません。シングルトンにさらに多くのフィールドがある場合、可変フィールドのセッターだけでなく、より多くのゲッターがあります。object
を使用してシングルトンを明示的に宣言するための明確で便利な方法を提供します。キーワード。内部を見るとわかるように、手頃な価格で自然な方法で実装されています。結論
Java仮想マシン:クラッシュコース
.class
に変換します。 Java仮想マシンによって実行されるJavaバイトコードを含むファイル。 2つの言語が内部でどのように異なるかを理解するには、両方が対象としているシステムを理解する必要があります。ここでは、Java仮想マシンアーキテクチャのいくつかの主要な要素、クラスファイル構造、およびアセンブラの基本について簡単に説明します。
javap
を使用したクラスファイルの逆コンパイル
コンスタントプール
フィールドテーブルとメソッドテーブル
JVMバイトコード
メソッド呼び出しと呼び出しスタック
オペランドスタックでの実行
ローカル変数
トップに戻る javap
を使用したクラスファイルの逆コンパイルjavap
が付属しています.class
を逆コンパイルするコマンドラインユーティリティ人間が読める形式にファイルします。 ScalaクラスファイルとJavaクラスファイルはどちらも同じJVMを対象としているため、javap
Scalaによってコンパイルされたクラスファイルを調べるために使用できます。// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }
scalac RegularPolygon.scala
でコンパイルするRegularPolygon.class
を生成します。次にjavap RegularPolygon.class
を実行すると次のように表示されます。$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
-p
を追加するオプションにはプライベートメンバーが含まれます:$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
-c
を追加しましょう。オプション:$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }
-v
を使用する必要があります。または-verbose
javap -p -v RegularPolygon.class
のようなオプション:コンスタントプール
// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...
365
次のバイトコードを持つ定数プールエントリがある場合があります。x03 00 00 01 6D
x03
は、エントリタイプCONSTANT_Integer
を識別します。これは、次の4バイトに整数の値が含まれていることをリンカに通知します。 (16進数の365はx16D
であることに注意してください)。これが定数プールの14番目のエントリである場合、javap -v
次のようにレンダリングされます:#14 = Integer 365
println( 'Calculating perimeter...' )
CONSTANT_String
のエントリです。 、およびタイプCONSTANT_Utf8
の別のエントリ。タイプConstant_UTF8
のエントリ文字列値の実際のUTF8表現が含まれます。タイプCONSTANT_String
のエントリCONSTANT_Utf8
への参照が含まれていますエントリ:#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...
Utf8
のエントリを参照する他のタイプの定数プールエントリがあるため、このような複雑さが必要です。タイプString
のエントリではありません。たとえば、クラス属性を参照すると、CONSTANT_Fieldref
が生成されます。タイプ。クラス名、属性名、および属性タイプへの一連の参照が含まれています。 #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I
フィールドテーブルとメソッドテーブル
JVMバイトコード
javap
-c
でオプションは、コンパイルされたメソッドの実装を出力に含めます。 RegularPolygon.class
を調べるとこの方法でファイルすると、getPerimeter()
の次の出力が表示されます。方法:public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn
xB2 00 17 x12 19 xB6 00 1D x27 ...
javap
バイトコードを人間が読める形式に変換すると、次のように表示されます。
#23
などのポンド記号で表示されるオペランドは、定数プール内のエントリーへの参照です。ご覧のとおり、javap
また、出力に役立つコメントを生成し、プールから正確に参照されているものを識別します。メソッド呼び出しと呼び出しスタック
オペランドスタックでの実行
sideLength * this.numSides
getPerimeter()
で次のようにコンパイルされます方法: 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul
dload_1
は、オブジェクト参照をローカル変数テーブル(次に説明)のスロット1からオペランドスタックにプッシュします。この場合、これはメソッド引数sideLength
.-次の命令aload_0
は、ローカル変数テーブルのスロット0にあるオブジェクト参照をオペランドスタックにプッシュします。実際には、これはほとんどの場合、現在のクラスであるthis
への参照です。invokevirtual #31
を実行する次の呼び出しnumSides()
のスタックが設定されます。 invokevirtual
最上位のオペランド(this
への参照)をスタックからポップして、メソッドを呼び出す必要のあるクラスを識別します。メソッドが戻ると、その結果はスタックにプッシュされます。numSides
)は整数形式です。別のdouble値を乗算するには、double浮動小数点形式に変換する必要があります。命令i2d
整数値をスタックからポップし、浮動小数点形式に変換して、スタックにプッシュバックします。this.numSides
の浮動小数点結果が含まれています。上にsideLength
の値が続きますメソッドに渡された引数。 dmul
これらの上位2つの値をスタックからポップし、それらに対して浮動小数点乗算を実行して、結果をスタックにプッシュします。ローカル変数
this
への参照が含まれますオブジェクト、メソッドが呼び出されたときに渡された引数、およびメソッド本体内で宣言されたローカル変数。実行中javap
-v
でオプションには、ローカル変数テーブルなど、各メソッドのスタックフレームの設定方法に関する情報が含まれます。public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D
this
で、タイプはRegularPolygon
です。これは、メソッド自体のクラスへの参照です。スロット1の変数の名前はsideLength
で、タイプはD
です。 (ダブルを示します)。これは、getPerimeter()
に渡される引数です。方法。iload_1
、fstore_2
、aload [n]
などの命令は、オペランドスタックとローカル変数テーブルの間でさまざまなタイプのローカル変数を転送します。通常、表の最初の項目はthis
への参照であるため、命令aload_0
独自のクラスで動作するすべてのメソッドで一般的に見られます。m4()
このフィールドの値が0であるかどうかをチェックし、m4
であることを示します。まだ初期化されていません。この場合、m4$lzycompute()
が呼び出され、m4
が入力されますそしてその値を返します。このプライベートメソッドは、bitmap
の値も設定します次回はScalaJVMバイトコードで手を汚す
前提条件
javap
が用意されており、ここで使用します。 javap
の簡単なデモンストレーション作品が含まれています 下部のガイドで 。デフォルトのゲッターとセッター
class Person(val name:String) { }
Person
。このファイルをscalac
でコンパイルすると、$ javap -p Person.class
が実行されます。私たちに与える:Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }
val
を置き換える場合var
でPerson
でソースを作成して再コンパイルしてから、フィールドのfinal
修飾子が削除され、setterメソッドも追加されます。Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }
val
またはvar
がクラス本体内で定義されると、対応するプライベートフィールドとアクセサメソッドが作成され、インスタンスの作成時に適切に初期化されます。val
およびvar
フィールドは、いくつかの変数が中間値を格納するためにクラスレベルで使用され、プログラマーが直接アクセスしない場合、そのような各フィールドを初期化すると、クラスのフットプリントに1つから2つのメソッドが追加されることを意味します。 private
を追加するこのようなフィールドの修飾子は、対応するアクセサーが削除されることを意味するものではありません。彼らはただプライベートになります。変数と関数の定義
m()
があり、この関数への3つの異なるScalaスタイルの参照を作成するとします。class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }
m
へのこれらの各参照はどうですか構築されましたか?いつm
いずれの場合も実行されますか?結果のバイトコードを見てみましょう。次の出力は、javap -v Person.class
の結果を示しています。 (多くの余分な出力を省略):Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return
m()
への参照がわかります。インデックス#30
に格納されます。コンストラクターコードでは、このメソッドが初期化中に2回呼び出され、命令invokevirtual #30
が使用されていることがわかります。最初にバイトオフセット11に表示され、次にオフセット19に表示されます。最初の呼び出しの後に命令putfield #22
が続きます。これは、このメソッドの結果を、インデックスm1
によって参照されるフィールド#22
に割り当てます。定数プールで。 2回目の呼び出しの後に同じパターンが続きます。今回は、m2
でインデックス付けされたフィールド#24
に値を割り当てます。定数プールで。val
で定義された変数にメソッドを割り当てます。またはvar
のみを割り当てます 結果 その変数へのメソッドの。メソッドm1()
がわかりますおよびm2()
作成されるのは、これらの変数の単なるゲッターです。 var m2
の場合、セッターm2_$eq(int)
もわかります。が作成され、他のセッターと同じように動作し、フィールドの値を上書きします。def
を使用する別の結果が得られます。返すフィールド値をフェッチするのではなく、メソッドm3()
命令invokevirtual #30
も含まれています。つまり、このメソッドが呼び出されるたびに、m()
が呼び出され、このメソッドの結果が返されます。val
、var
、およびdef
を介して簡単に指定できます。 Javaでは、必要なセッターとゲッターを明示的に実装する必要があり、そのような手動で記述されたボイラープレートコードは、表現力がはるかに低く、エラーが発生しやすくなります。怠惰な価値観
lazy val m4 = m
javap -p -v Person.class
これで、次のことが明らかになります。Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn
m4
必要になるまで計算されません。特別なプライベートメソッドm4$lzycompute()
レイジー値を計算するために生成され、フィールドbitmap$0
その状態を追跡します。メソッドm4()
このフィールドの値が0であるかどうかをチェックし、m4
であることを示します。まだ初期化されていません。この場合、m4$lzycompute()
が呼び出され、m4
が入力されますそしてその値を返します。このプライベートメソッドは、bitmap$0
の値も設定します次回はm4()
が呼び出されると、初期化メソッドの呼び出しがスキップされ、代わりにm4
の値が返されます。monitorenter
/ monitorexit
を使用します。指示のペア。この同期のパフォーマンスオーバーヘッドは、遅延値の最初の読み取り時にのみ発生するため、この方法は引き続き有効です。価値として機能する
class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }
Printer
クラスには、タイプoutput
の1つのフィールドString => Unit
があります。String
を受け取る関数です。タイプUnit
のオブジェクトを返します(Javaのvoid
に似ています)。 mainメソッドでは、これらのオブジェクトの1つを作成し、このフィールドを指定された文字列を出力する無名関数として割り当てます。Hello.class
mainメソッドが単にHello$.main()
を呼び出すラッパークラスです。public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return
Hello$.class
mainメソッドの実際の実装が含まれています。そのバイトコードを確認するには、$
を正しくエスケープしていることを確認してください。コマンドシェルの規則に従って、特殊文字としての解釈を避けるために:public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return
Printer
を作成します。次に、匿名関数Hello$$anonfun$1
を含むs => println(s)
を作成します。 Printer
このオブジェクトでoutput
として初期化されますフィールド。次に、このフィールドはスタックにロードされ、オペランド'Hello'
で実行されます。Hello$$anonfun$1.class
を見てみましょう。 ScalaのFunction1
を拡張していることがわかります(AbstractFunction1
として)apply()
を実装する方法。実際には、2つのapply()
を作成しますメソッドは、一方が他方をラップし、一緒に型チェックを実行し(この場合、入力がString
であること)、無名関数を実行します(入力をprintln()
で出力します)。public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn
Hello$.main()
を振り返って上記の方法では、オフセット21で、無名関数の実行がそのapply( Object )
の呼び出しによってトリガーされることがわかります。方法。Printer.class
のバイトコードを見てみましょう。public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return
val
と同じように扱われることがわかります。変数。クラスフィールドoutput
とゲッターoutput()
に格納されます創造された。唯一の違いは、この変数がScalaインターフェースを実装する必要があることですscala.Function1
(これはAbstractFunction1
が行います)。Scalaの特性
trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }
Similarity.class
と、デフォルトの実装を提供する合成クラスSimilarity$class.class
です。public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); }
public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return
isNotSimilar
を呼び出すと、Scalaコンパイラーはバイトコード命令invokestatic
を生成します。付随するクラスによって提供される静的メソッドを呼び出します。super.methodName()
を呼び出す場合があります。次の特性に制御を渡すため。 Scalaコンパイラがそのような呼び出しに遭遇すると、次のようになります。
invokestatic
を生成します命令。シングルトン
object
を使用してシングルトンクラスの明示的な定義を提供します。次のシングルトンクラスについて考えてみましょう。object Config { val home_dir = '/home/user' }
Config.class
非常に単純なものです:public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn
Config$
シングルトンの機能を組み込むクラス。 javap -p -c
でそのクラスを調べる次のバイトコードを生成します。public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return
MODULE$
。{}
(クラス初期化子とも呼ばれます)およびConfig$
の初期化に使用されるプライベートメソッドMODULE$
フィールドをデフォルト値に設定しますhome_dir
のゲッターメソッド。この場合、それは1つの方法にすぎません。シングルトンにさらに多くのフィールドがある場合、可変フィールドのセッターだけでなく、より多くのゲッターがあります。object
を使用してシングルトンを明示的に宣言するための明確で便利な方法を提供します。キーワード。内部を見るとわかるように、手頃な価格で自然な方法で実装されています。結論
Java仮想マシン:クラッシュコース
.class
に変換します。 Java仮想マシンによって実行されるJavaバイトコードを含むファイル。 2つの言語が内部でどのように異なるかを理解するには、両方が対象としているシステムを理解する必要があります。ここでは、Java仮想マシンアーキテクチャのいくつかの主要な要素、クラスファイル構造、およびアセンブラの基本について簡単に説明します。
javap
を使用したクラスファイルの逆コンパイル
コンスタントプール
フィールドテーブルとメソッドテーブル
JVMバイトコード
メソッド呼び出しと呼び出しスタック
オペランドスタックでの実行
ローカル変数
トップに戻る javap
を使用したクラスファイルの逆コンパイルjavap
が付属しています.class
を逆コンパイルするコマンドラインユーティリティ人間が読める形式にファイルします。 ScalaクラスファイルとJavaクラスファイルはどちらも同じJVMを対象としているため、javap
Scalaによってコンパイルされたクラスファイルを調べるために使用できます。// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }
scalac RegularPolygon.scala
でコンパイルするRegularPolygon.class
を生成します。次にjavap RegularPolygon.class
を実行すると次のように表示されます。$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
-p
を追加するオプションにはプライベートメンバーが含まれます:$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
-c
を追加しましょう。オプション:$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }
-v
を使用する必要があります。または-verbose
javap -p -v RegularPolygon.class
のようなオプション:コンスタントプール
// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...
365
次のバイトコードを持つ定数プールエントリがある場合があります。x03 00 00 01 6D
x03
は、エントリタイプCONSTANT_Integer
を識別します。これは、次の4バイトに整数の値が含まれていることをリンカに通知します。 (16進数の365はx16D
であることに注意してください)。これが定数プールの14番目のエントリである場合、javap -v
次のようにレンダリングされます:#14 = Integer 365
println( 'Calculating perimeter...' )
CONSTANT_String
のエントリです。 、およびタイプCONSTANT_Utf8
の別のエントリ。タイプConstant_UTF8
のエントリ文字列値の実際のUTF8表現が含まれます。タイプCONSTANT_String
のエントリCONSTANT_Utf8
への参照が含まれていますエントリ:#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...
Utf8
のエントリを参照する他のタイプの定数プールエントリがあるため、このような複雑さが必要です。タイプString
のエントリではありません。たとえば、クラス属性を参照すると、CONSTANT_Fieldref
が生成されます。タイプ。クラス名、属性名、および属性タイプへの一連の参照が含まれています。 #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I
フィールドテーブルとメソッドテーブル
JVMバイトコード
javap
-c
でオプションは、コンパイルされたメソッドの実装を出力に含めます。 RegularPolygon.class
を調べるとこの方法でファイルすると、getPerimeter()
の次の出力が表示されます。方法:public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn
xB2 00 17 x12 19 xB6 00 1D x27 ...
javap
バイトコードを人間が読める形式に変換すると、次のように表示されます。
#23
などのポンド記号で表示されるオペランドは、定数プール内のエントリーへの参照です。ご覧のとおり、javap
また、出力に役立つコメントを生成し、プールから正確に参照されているものを識別します。メソッド呼び出しと呼び出しスタック
オペランドスタックでの実行
sideLength * this.numSides
getPerimeter()
で次のようにコンパイルされます方法: 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul
dload_1
は、オブジェクト参照をローカル変数テーブル(次に説明)のスロット1からオペランドスタックにプッシュします。この場合、これはメソッド引数sideLength
.-次の命令aload_0
は、ローカル変数テーブルのスロット0にあるオブジェクト参照をオペランドスタックにプッシュします。実際には、これはほとんどの場合、現在のクラスであるthis
への参照です。invokevirtual #31
を実行する次の呼び出しnumSides()
のスタックが設定されます。 invokevirtual
最上位のオペランド(this
への参照)をスタックからポップして、メソッドを呼び出す必要のあるクラスを識別します。メソッドが戻ると、その結果はスタックにプッシュされます。numSides
)は整数形式です。別のdouble値を乗算するには、double浮動小数点形式に変換する必要があります。命令i2d
整数値をスタックからポップし、浮動小数点形式に変換して、スタックにプッシュバックします。this.numSides
の浮動小数点結果が含まれています。上にsideLength
の値が続きますメソッドに渡された引数。 dmul
これらの上位2つの値をスタックからポップし、それらに対して浮動小数点乗算を実行して、結果をスタックにプッシュします。ローカル変数
this
への参照が含まれますオブジェクト、メソッドが呼び出されたときに渡された引数、およびメソッド本体内で宣言されたローカル変数。実行中javap
-v
でオプションには、ローカル変数テーブルなど、各メソッドのスタックフレームの設定方法に関する情報が含まれます。public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D
this
で、タイプはRegularPolygon
です。これは、メソッド自体のクラスへの参照です。スロット1の変数の名前はsideLength
で、タイプはD
です。 (ダブルを示します)。これは、getPerimeter()
に渡される引数です。方法。iload_1
、fstore_2
、aload [n]
などの命令は、オペランドスタックとローカル変数テーブルの間でさまざまなタイプのローカル変数を転送します。通常、表の最初の項目はthis
への参照であるため、命令aload_0
独自のクラスで動作するすべてのメソッドで一般的に見られます。m4()
が呼び出されると、初期化メソッドの呼び出しがスキップされ、代わりにm4
の値が返されます。
Scalaがここで生成するバイトコードは、スレッドセーフで効果的であるように設計されています。スレッドセーフにするために、レイジーコンピューティングメソッドはmonitorenter
/ monitorexit
を使用します。指示のペア。この同期のパフォーマンスオーバーヘッドは、遅延値の最初の読み取り時にのみ発生するため、この方法は引き続き有効です。
遅延値の状態を示すために必要なビットは1つだけです。したがって、レイジー値が32個以下の場合、1つのintフィールドでそれらすべてを追跡できます。ソースコードで複数の遅延値が定義されている場合、上記のバイトコードは、この目的のためにビットマスクを実装するようにコンパイラによって変更されます。
繰り返しになりますが、Scalaを使用すると、Javaで明示的に実装する必要がある特定の種類の動作を簡単に利用できるため、労力を節約し、タイプミスのリスクを減らすことができます。
それでは、次のScalaソースコードを見てみましょう。
class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }
Printer
クラスには、タイプoutput
の1つのフィールドString => Unit
があります。String
を受け取る関数です。タイプUnit
のオブジェクトを返します(Javaのvoid
に似ています)。 mainメソッドでは、これらのオブジェクトの1つを作成し、このフィールドを指定された文字列を出力する無名関数として割り当てます。
このコードをコンパイルすると、4つのクラスファイルが生成されます。
Hello.class
mainメソッドが単にHello$.main()
を呼び出すラッパークラスです。
セレンスクリプトのページオブジェクトモデル
public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return
隠されたHello$.class
mainメソッドの実際の実装が含まれています。そのバイトコードを確認するには、$
を正しくエスケープしていることを確認してください。コマンドシェルの規則に従って、特殊文字としての解釈を避けるために:
public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun 7: dup 8: invokespecial #19 // Method Hello$$anonfun.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return
このメソッドはPrinter
を作成します。次に、匿名関数Hello$$anonfun
を含むs => println(s)
を作成します。 Printer
このオブジェクトでoutput
として初期化されますフィールド。次に、このフィールドはスタックにロードされ、オペランド'Hello'
で実行されます。
以下の無名関数クラスHello$$anonfun.class
を見てみましょう。 ScalaのFunction1
を拡張していることがわかります(AbstractFunction1
として)apply()
を実装する方法。実際には、2つのapply()
を作成しますメソッドは、一方が他方をラップし、一緒に型チェックを実行し(この場合、入力がString
であること)、無名関数を実行します(入力をprintln()
で出力します)。
public final class Hello$$anonfun extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn
Hello$.main()
を振り返って上記の方法では、オフセット21で、無名関数の実行がそのapply( Object )
の呼び出しによってトリガーされることがわかります。方法。
最後に、完全を期すために、Printer.class
のバイトコードを見てみましょう。
public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return
ここでの無名関数は他のval
と同じように扱われることがわかります。変数。クラスフィールドoutput
とゲッターoutput()
に格納されます創造された。唯一の違いは、この変数がScalaインターフェースを実装する必要があることですscala.Function1
(これはAbstractFunction1
が行います)。
したがって、このエレガントなScala機能のコストは、値として使用できる単一の無名関数を表現および実行するために作成された、基礎となるユーティリティクラスです。特定のアプリケーションにとってそれが何を意味するのかを理解するには、そのような機能の数とVM実装の詳細を考慮する必要があります。
Scalaの内部を理解する:この強力な言語がJVMバイトコードでどのように実装されているかを探ります。 つぶやきScalaの特性は、Javaのインターフェースに似ています。次の特性は、2つのメソッド署名を定義し、2番目のメソッドのデフォルトの実装を提供します。それがどのように実装されているか見てみましょう:
trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }
2つのエンティティが生成されます。両方のメソッドを宣言するインターフェイスであるSimilarity.class
と、デフォルトの実装を提供する合成クラスSimilarity$class.class
です。
public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); }
public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return
クラスがこの特性を実装し、メソッドisNotSimilar
を呼び出すと、Scalaコンパイラーはバイトコード命令invokestatic
を生成します。付随するクラスによって提供される静的メソッドを呼び出します。
複雑なポリモーフィズムと継承構造は、特性から作成される場合があります。たとえば、複数のトレイトと実装クラスがすべて、同じシグネチャを持つメソッドをオーバーライドして、super.methodName()
を呼び出す場合があります。次の特性に制御を渡すため。 Scalaコンパイラがそのような呼び出しに遭遇すると、次のようになります。
invokestatic
を生成します命令。したがって、トレイトの強力な概念が、大きなオーバーヘッドを引き起こさない方法でJVMレベルで実装されていることがわかります。また、Scalaプログラマーは、実行時にコストがかかりすぎることを心配せずにこの機能を楽しむことができます。
Scalaは、キーワードobject
を使用してシングルトンクラスの明示的な定義を提供します。次のシングルトンクラスについて考えてみましょう。
object Config { val home_dir = '/home/user' }
コンパイラは2つのクラスファイルを生成します。
Config.class
非常に単純なものです:
public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn
これは、合成のデコレータですConfig$
シングルトンの機能を組み込むクラス。 javap -p -c
でそのクラスを調べる次のバイトコードを生成します。
public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return
以下で構成されています。
MODULE$
。{}
(クラス初期化子とも呼ばれます)およびConfig$
の初期化に使用されるプライベートメソッドMODULE$
フィールドをデフォルト値に設定しますhome_dir
のゲッターメソッド。この場合、それは1つの方法にすぎません。シングルトンにさらに多くのフィールドがある場合、可変フィールドのセッターだけでなく、より多くのゲッターがあります。シングルトンは人気があり便利なデザインパターンです。 Java言語は、言語レベルでそれを指定する直接的な方法を提供しません。むしろ、Javaソースに実装するのは開発者の責任です。一方、Scalaは、object
を使用してシングルトンを明示的に宣言するための明確で便利な方法を提供します。キーワード。内部を見るとわかるように、手頃な価格で自然な方法で実装されています。
これで、Scalaがいくつかの暗黙的および関数型プログラミング機能を洗練されたJavaバイトコード構造にコンパイルする方法を見てきました。 Scalaの内部の仕組みを垣間見ることで、Scalaの力をより深く理解することができ、この強力な言語を最大限に活用するのに役立ちます。
テーブル内のデータが冗長になった場合:
また、自分たちで言語を探索するためのツールもあります。ケースクラス、カリー化、リスト内包表記など、この記事では取り上げていないScala構文の便利な機能がたくさんあります。これらの構造のScalaの実装を自分で調査して、次のレベルのScala忍者になる方法を学ぶことをお勧めします。
Javaコンパイラと同様に、Scalaコンパイラはソースコードを.class
に変換します。 Java仮想マシンによって実行されるJavaバイトコードを含むファイル。 2つの言語が内部でどのように異なるかを理解するには、両方が対象としているシステムを理解する必要があります。ここでは、Java仮想マシンアーキテクチャのいくつかの主要な要素、クラスファイル構造、およびアセンブラの基本について簡単に説明します。
このガイドでは、上記の記事に沿ってフォローできるようにするための最小限の内容のみを取り上げていることに注意してください。 JVMの多くの主要コンポーネントについてはここでは説明していませんが、完全な詳細は公式ドキュメントにあります。 ここに 。
javap
を使用したクラスファイルの逆コンパイル
コンスタントプール
フィールドテーブルとメソッドテーブル
JVMバイトコード
メソッド呼び出しと呼び出しスタック
オペランドスタックでの実行
ローカル変数
トップに戻る
javap
を使用したクラスファイルの逆コンパイルJavaにはjavap
が付属しています.class
を逆コンパイルするコマンドラインユーティリティ人間が読める形式にファイルします。 ScalaクラスファイルとJavaクラスファイルはどちらも同じJVMを対象としているため、javap
Scalaによってコンパイルされたクラスファイルを調べるために使用できます。
次のソースコードをコンパイルしましょう。
// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }
これをscalac RegularPolygon.scala
でコンパイルするRegularPolygon.class
を生成します。次にjavap RegularPolygon.class
を実行すると次のように表示されます。
$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
これはクラスファイルの非常に単純な内訳であり、クラスのパブリックメンバーの名前とタイプを単純に示しています。 -p
を追加するオプションにはプライベートメンバーが含まれます:
$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
これはまだ多くの情報ではありません。メソッドがJavaバイトコードでどのように実装されているかを確認するために、-c
を追加しましょう。オプション:
最高の有料出会い系サイト2015
$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }
それはもう少し面白いです。ただし、実際に全体像を把握するには、-v
を使用する必要があります。または-verbose
javap -p -v RegularPolygon.class
のようなオプション:
ここで、最終的にクラスファイルに実際に何が含まれているかがわかります。これはどういう意味ですか?最も重要な部分のいくつかを見てみましょう。
C ++アプリケーションの開発サイクルには、コンパイルとリンケージの段階が含まれます。リンケージは実行時に発生するため、Javaの開発サイクルは明示的なリンケージステージをスキップします。クラスファイルは、このランタイムリンクをサポートする必要があります。つまり、ソースコードが任意のフィールドまたはメソッドを参照する場合、結果のバイトコードは関連する参照をシンボリック形式で保持し、アプリケーションがメモリにロードされ、ランタイムリンカーによって実際のアドレスを解決できるようになったら逆参照できるようにする必要があります。このシンボリックフォームには、次のものが含まれている必要があります。
クラスファイル形式の仕様には、ファイルの「 定数プール 、リンカが必要とするすべての参照の表。さまざまなタイプのエントリが含まれています。
// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...
各エントリの最初のバイトは、エントリのタイプを示す数値タグです。残りのバイトは、エントリの値に関する情報を提供します。バイト数とその解釈の規則は、最初のバイトで示されるタイプによって異なります。
たとえば、定数整数を使用するJavaクラス365
次のバイトコードを持つ定数プールエントリがある場合があります。
x03 00 00 01 6D
最初のバイトx03
は、エントリタイプCONSTANT_Integer
を識別します。これは、次の4バイトに整数の値が含まれていることをリンカに通知します。 (16進数の365はx16D
であることに注意してください)。これが定数プールの14番目のエントリである場合、javap -v
次のようにレンダリングされます:
#14 = Integer 365
多くの定数型は、定数プール内の他の場所にある、より「プリミティブな」定数型への参照で構成されています。たとえば、サンプルコードには次のステートメントが含まれています。
println( 'Calculating perimeter...' )
文字列定数を使用すると、定数プールに2つのエントリが生成されます。1つはタイプCONSTANT_String
のエントリです。 、およびタイプCONSTANT_Utf8
の別のエントリ。タイプConstant_UTF8
のエントリ文字列値の実際のUTF8表現が含まれます。タイプCONSTANT_String
のエントリCONSTANT_Utf8
への参照が含まれていますエントリ:
#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...
タイプUtf8
のエントリを参照する他のタイプの定数プールエントリがあるため、このような複雑さが必要です。タイプString
のエントリではありません。たとえば、クラス属性を参照すると、CONSTANT_Fieldref
が生成されます。タイプ。クラス名、属性名、および属性タイプへの一連の参照が含まれています。
#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I
定数プールの詳細については、を参照してください。 JVMドキュメント 。
クラスファイルには、 フィールドテーブル クラスで定義された各フィールド(つまり、属性)に関する情報が含まれています。これらは、フィールドの名前とタイプ、およびアクセス制御フラグとその他の関連データを説明する定数プールエントリへの参照です。
似たような メソッドテーブル クラスファイルに存在します。ただし、名前とタイプの情報に加えて、非抽象メソッドごとに、JVMによって実行される実際のバイトコード命令と、以下で説明するメソッドのスタックフレームによって使用されるデータ構造が含まれます。
JVMは、独自の内部命令セットを使用して、コンパイルされたコードを実行します。実行中javap
-c
でオプションは、コンパイルされたメソッドの実装を出力に含めます。 RegularPolygon.class
を調べるとこの方法でファイルすると、getPerimeter()
の次の出力が表示されます。方法:
public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn
実際のバイトコードは次のようになります。
xB2 00 17 x12 19 xB6 00 1D x27 ...
各命令は1バイトで始まります オペコード 特定の命令の形式に応じて、JVM命令を識別し、その後に操作対象の0個以上の命令オペランドを指定します。これらは通常、定数値、または定数プールへの参照のいずれかです。 javap
バイトコードを人間が読める形式に変換すると、次のように表示されます。
#23
などのポンド記号で表示されるオペランドは、定数プール内のエントリーへの参照です。ご覧のとおり、javap
また、出力に役立つコメントを生成し、プールから正確に参照されているものを識別します。
以下では、いくつかの一般的な手順について説明します。完全なJVM命令セットの詳細については、 ドキュメンテーション 。
各メソッド呼び出しは、ローカルで宣言された変数やメソッドに渡された引数などを含む、独自のコンテキストで実行できる必要があります。一緒に、これらは構成します スタックフレーム 。メソッドを呼び出すと、新しいフレームが作成され、その上に配置されます。 コールスタック 。メソッドが戻ると、現在のフレームが呼び出しスタックから削除されて破棄され、メソッドが呼び出される前に有効だったフレームが復元されます。
ccorpとscorpの違いは何ですか
スタックフレームには、いくつかの異なる構造が含まれています。 2つの重要なものは オペランドスタック そしてその ローカル変数テーブル 、次に説明します。
多くのJVM命令は、フレームで動作します オペランドスタック 。これらの命令は、バイトコードで定数オペランドを明示的に指定するのではなく、オペランドスタックの最上位の値を入力として受け取ります。通常、これらの値はプロセスでスタックから削除されます。一部の命令では、スタックの最上位に新しい値も配置されます。このようにして、JVM命令を組み合わせて、複雑な操作を実行できます。たとえば、次の式です。
sideLength * this.numSides
getPerimeter()
で次のようにコンパイルされます方法:
8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul
dload_1
は、オブジェクト参照をローカル変数テーブル(次に説明)のスロット1からオペランドスタックにプッシュします。この場合、これはメソッド引数sideLength
.-次の命令aload_0
は、ローカル変数テーブルのスロット0にあるオブジェクト参照をオペランドスタックにプッシュします。実際には、これはほとんどの場合、現在のクラスであるthis
への参照です。invokevirtual #31
を実行する次の呼び出しnumSides()
のスタックが設定されます。 invokevirtual
最上位のオペランド(this
への参照)をスタックからポップして、メソッドを呼び出す必要のあるクラスを識別します。メソッドが戻ると、その結果はスタックにプッシュされます。numSides
)は整数形式です。別のdouble値を乗算するには、double浮動小数点形式に変換する必要があります。命令i2d
整数値をスタックからポップし、浮動小数点形式に変換して、スタックにプッシュバックします。this.numSides
の浮動小数点結果が含まれています。上にsideLength
の値が続きますメソッドに渡された引数。 dmul
これらの上位2つの値をスタックからポップし、それらに対して浮動小数点乗算を実行して、結果をスタックにプッシュします。メソッドが呼び出されると、そのスタックフレームの一部として新しいオペランドスタックが作成され、そこで操作が実行されます。ここでは用語に注意する必要があります。「スタック」という言葉は、 コールスタック 、メソッド実行または特定のフレームのコンテキストを提供するフレームのスタック オペランドスタック 、JVM命令が動作する。
各スタックフレームは、 ローカル変数 。これには通常、this
への参照が含まれますオブジェクト、メソッドが呼び出されたときに渡された引数、およびメソッド本体内で宣言されたローカル変数。実行中javap
-v
でオプションには、ローカル変数テーブルなど、各メソッドのスタックフレームの設定方法に関する情報が含まれます。
public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D
この例では、2つのローカル変数があります。スロット0の変数の名前はthis
で、タイプはRegularPolygon
です。これは、メソッド自体のクラスへの参照です。スロット1の変数の名前はsideLength
で、タイプはD
です。 (ダブルを示します)。これは、getPerimeter()
に渡される引数です。方法。
iload_1
、fstore_2
、aload [n]
などの命令は、オペランドスタックとローカル変数テーブルの間でさまざまなタイプのローカル変数を転送します。通常、表の最初の項目はthis
への参照であるため、命令aload_0
独自のクラスで動作するすべてのメソッドで一般的に見られます。
これで、JVMの基本についてのウォークスルーは終わりです。 メインの記事に戻るには、ここをクリックしてください。