この記事では、Java の静的ディスパッチと動的ディスパッチの概要 (コード例) を紹介します。必要な方は参考にしていただければ幸いです。
最近JVMの知識を見直していて、静的ディスパッチと動的ディスパッチの理解が少しわかりにくいので、自分でコードを書いて分析の知識を定着させてみました。
次のコード部分があります。各コードは何を出力しますか?
package com.khlin.my.test; class Base { public static void foo() { System.out.println("Base.foo() invoked"); } public void bar(int c) { System.out.println("Base.bar(int) invoked"); } public void bar(Character c) { System.out.println("Base.bar(Character) invoked"); } public void baz(Object o) { System.out.println("Base.baz(Object) invoked"); } public void baz(Integer i) { System.out.println("Base.baz(Integer) invoked"); } } class Child extends Base { public static void foo() { System.out.println("Child.foo() invoked"); } public void bar(Character c) { System.out.println("Child.bar(Character) invoked"); } public void bar(char c) { System.out.println("Child.bar(char) invoked"); } } public class App { public static void main(String[] args) { Base child = new Child(); System.out.println("第1段输出:"); child.foo(); child.bar(new Character('C')); System.out.println("第2段输出:"); Object integer = new Integer(100); child.baz(integer); System.out.println("第3段输出:"); child.bar('C'); } }
コードのコンパイルからメソッド呼び出しまでのプロセス全体を簡単に紹介します。
· コンパイル
まず最初の段落の出力を確認します。child.foo() は親クラスの静的メソッドを呼び出しています。それともサブクラス?
コンパイル段階で、静的ディスパッチが発生します。
1 Base child = new Child();
オブジェクトを作成するとき、上に示したように、Base は変数の静的型 (Static Type) または外観型と呼ばれます。 (見かけの型) に続く子を変数の実際の型 (実際の型) と呼びます。
メソッド実行バージョンを見つけるために静的型に依存するすべてのディスパッチ アクションは、静的ディスパッチと呼ばれます。 静的ディスパッチの一般的な用途はメソッドのオーバーロードです。これはコンパイル フェーズ中に発生するため、静的にディスパッチされると判断されたアクションは実際には仮想マシンによって実行されません。
メソッドの受信者 (Reciever) とメソッドのパラメーターは、メソッドの変数と総称されます。ディスパッチが基づく変数の種類に応じて、ディスパッチは単一のディスパッチと複数のディスパッチに分けられます。急送。
静的ディスパッチでは、対象メソッドを選択する基準が 2 つあります。1 つは静的型が Base か Child であるか、もう 1 つはメソッドのパラメーターの型です。したがって、静的ディスパッチはマルチディスパッチです。
次に、「出力セクション 1」のコードによって生成される命令を見てみましょう。次の結果は、javap -v App.class 命令によって得られます。18 行目と 31 行目で 2 つの命令のシンボリック参照が確認できます。これは、上記の分析と一致しています。つまり、子の静的型は Base であるため、メソッドはBase クラスの値が選択されます。None を介してパラメータと Character タイプによって、メソッドのバージョンが決まります。
しかし、結局のところ、この 2 つの動作は異なります。 child.foo() は静的型 Base の foo() を呼び出しますが、child.bar(new Character( 'C ')) は、実際の型 Child のメソッドを呼び出します。
理由は、invokestatic と invokevirtual の 2 つの命令が異なるためです。
Java 仮想マシンには、5 つのメソッド呼び出しバイトコード命令が用意されています。
invokestatic: 静的メソッドの呼び出し
invokespecial: インスタンス コンストラクター
invokevirtual: すべての仮想メソッドを呼び出します。
invokeinterface: このインターフェイスを実装するオブジェクトであるインターフェイス メソッドを呼び出します。実行時に決定されます
invokedynamic: まず、実行時に呼び出しポイント修飾子によって参照されるメソッドを動的に解析し、その後メソッドを実行します。 命令とディスパッチ ロジックは Java 仮想マシン内で固定化されます。 、invokedynamic はユーザーが設定した起動方法によって決まります。
具体的な理由は、命令が異なると次の段階 (クラス読み込みの解析) で動作が異なるためです。それは今は脇に置いて、2 番目の段落で出力される命令を見てみましょう。
静的ディスパッチでは、呼び出されるメソッドのバージョンは、メソッドに渡されるパラメータの静的型に基づいて決定されることがわかります。メソッド、 baz(Integer) メソッドがありますが、渡される整数パラメータの静的型は Object であるため、baz(Object) が呼び出されます。
段落 3 で出力された命令を見てみましょう。シンボル参照は依然として Base クラスのメソッドでなければならないことがわかります (ただし、Child クラスには同じパラメーターを持つ bar(char c) メソッドがあります)。 ) ですが、Base.(char型)メソッドに同一のパラメータが存在しない場合、エラーは報告されないのでしょうか?どのメソッドが呼び出されますか?
コンパイラはメソッドのオーバーロードされたバージョンを決定できますが、多くの場合、このオーバーロードされたバージョンが「唯一のもの」ではなく、1 つのみを決定できることが判明しました。 . 「より適切な」バージョン。
·クラス読み込み解析
解析フェーズは、仮想マシンが定数プール内のシンボル参照を直接参照に置き換えるプロセスです。 。
メソッドが invokestatic 命令と invokespecial 命令によって呼び出される限り、解析フェーズで決定できるのは呼び出しバージョンのみです。この条件を満たす 4 つのカテゴリは、静的メソッド、プライベート メソッド、インスタンス コンストラクターです。および親クラスのメソッドは、クラスがロードされるときにシンボル参照をメソッドへの直接参照に解決します。これらのメソッドは非仮想メソッドと呼ばれることがあり、他のメソッドは非仮想メソッドと呼ばれます (最終メソッドを除く)。
最終的に変更されたメソッドは invokevirtual 命令を使用して呼び出されますが、上書きできず、他のバージョンがないため、これも非仮想メソッドです。
出力の最初の段落に戻ります。 child.foo() は invokestatic 命令であるため、解析段階で直接参照に置き換えられ、特定のクラスが決定されるため、静的型 Base が使用されます。 .foo() が呼び出されます。
そして、child.bar(new Character('C')) は invokevirtual です。この段階では、呼び出されたメソッドのシグネチャは決定できますが、メソッドのレシーバーの実際の型はまだ決定できません。それは動的ディスパッチによって決定されます。 動的割り当ては、ボリューム効果が 1 つしかないため、単一割り当てになります。
#メソッド レシーバーの実際のタイプは、次の段階で決定されます。
#·実行時のメソッド呼び出し
メソッドの実行バージョンは、実際のバージョンに基づいて決定されます。実行時のタイプ ディスパッチプロセスは動的ディスパッチと呼ばれます。 最終出力は次のとおりです:以上がJava の静的ディスパッチと動的ディスパッチの概要 (コード例)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。