
public interface A { //... } public interface B { //... } public class C implements A { //... }
また、
A
および
B
用にオーバーロードされた
foo
メソッドを持つクラス このメソッドは、クラス
C
インスタンスから呼び出されます。
public class CompatibilityChecker { public String foo(A a) { return "A"; } public String foo(B b) { return "B"; } public static void main(String[] args) { CompatibilityChecker checker = new CompatibilityChecker(); System.out.println(checker.foo(new C())); } }
「A」が表示されることは明らかです。
C implements A, B
ていると言うと、コンパイルエラーが発生することも同様に明らかです( 後者について不明な場合は、メソッドの選択方法について読むことをお勧めします。たとえば、セクション15.12.2以降の標準 場所を説明するだけです )。
しかし、
C.java
のみを再コンパイルし、既存のクラスファイルから
CompatibilityChecker
を実行するとどうなるかは、より複雑な問題です。 に興味がありますか? カットをお願いします!
静的ディスパッチ
オーバーロードされたメソッドがコンパイル中に選択されることを知っている人は、この理由で、クラスファイルに呼び出すメソッドに関する情報がすぐに含まれるため、「A」が出力されることに気付くでしょう。 この仮定を確認してください。 public static void main(java.lang.String[]); Code: 0: new #4; //class CompatibilityChecker 3: dup 4: invokespecial #5; //Method "<init>":()V 7: astore_1 8: getstatic #6; //Field java/lang/System.out:Ljava/io/PrintStream; 11: aload_1 12: new #7; //class C 15: dup 16: invokespecial #8; //Method C."<init>":()V 19: invokevirtual #9; //Method foo:(LA;)Ljava/lang/String; 22: invokevirtual #10; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 25: return
実際、インデントされた命令19からわかるように、非常に具体的なメソッドが呼び出されています。 ただし、 検証者について聞いたことがある人は異議を唱え、彼が
C
が変更され、例外がスローされます。 幸いなことに、ベリファイアはクラスとインターフェースの構造の正確性のみをチェックし、クラスファイルのバージョンの対応はチェックしないため、それらは間違っています。
それでは、コードを実行して、最初の仮定が正しいことを確認しましょう。実際には「A」が表示されます。
さらに、仮想アドレステーブルに間違ったアドレスが存在する可能性があるため、
NoSuchMethodError
すべてが
NoSuchMethodError
破損することを想定でき
NoSuchMethodError
。
foo(A)
メソッドが呼び出され、仮想テーブルで唯一のメソッドであるため、この仮定も誤りです。 別のことは、相続人がそれを再定義したかどうかです...
動的ディスパッチ
次の3つのクラスがあるとします。 public class A { public String foo() { return "A"; } } public class B extends A { @Override public String foo() { return "B"; } } public class C extends A { @Override public String foo() { return super.foo() + "C"; } }
そして、さまざまな方法で
foo
を呼び出すクラス:
public class CompatibilityChecker { public static void main(String[] args) { A a = new A(); A ab = new B(); B bb = new B(); A ac = new C(); C cc = new C(); System.out.println(a.foo()); System.out.println(ab.foo()); System.out.println(bb.foo()); System.out.println(ac.foo()); System.out.println(cc.foo()); } }
もちろん、誰もが、実行時に再割り当てされた型がメソッドを選択するため、初期出力は次のようになることを知っています。
A B B AC AC
次は、
A
のファイルクラスを次のコードのコンパイル結果に置き換えることで、このトリックを実行するときです。
public class A { public String foo(Object dummy) { return "A"; } }
この場合に何が起こるかを理解することは非常に簡単です。 まず、
foo
がクラス
A
インスタンスで呼び出されるメソッドを呼び出そうと
A
と、必ず
NoSuchMethodError
飛び出します。 これらの試みの中には、クラス
C
super.foo()
への呼び出しもあります
C
第二に、前に見たように、
B.foo()
メソッドが正常に呼び出されます。
それでは、戦術を変えましょう。再び、
A.foo
そのままにして、
B
と
C
変更して、
foo
メソッドの再定義を完全に削除します。
public class B extends A {} public class C extends A {}
コードが実行されると、ダイナミックディスパッチは
A.foo
エントリを1つだけ検出するため、すべてのケースでそれを呼び出します。その結果、コンソールには文字「A」のみが表示され、例外は完全に
A.foo
ます。
B
と
C
メソッドを再定義することにより、研究を続けています。 開始後、予想どおり、動的ディスパッチは仮想テーブル内のすべてのレコードを検出し、すべてを再コンパイルして得られるものとまったく同じ出力を提供します。
不適切なタイプのフィールド
以前、メソッドのみで実験を試みました。 次に、フィールドに何が起こるかを見てみましょう。 int値とこのクラスの継承者を格納するクラスがあるとします。 public class A { int answer; } public class B extends A {}
そして、伝統的に、クラス
B
消費者:
public class CompatibilityChecker { public static void main(String[] args) { B b = new B(); b.answer = 42; } }
次に、同じ名前の独自のフィールドをクラス
B
追加します。
public class B extends A { String answer; }
CompatibilityChecker
用に生成されたバイトコードを見てみましょう。
public static void main(java.lang.String[]); Code: 0: new #2; //class B 3: dup 4: invokespecial #3; //Method B."<init>":()V 7: astore_1 8: aload_1 9: bipush 42 11: putfield #4; //Field B.answer:I 14: return
このリストはコメントで11字下げされているため、混乱を招く可能性があります。フィールドがBに属していることを示しているようです。したがって、
B
を再コンパイルするとエラーが発生すると想定する必要があります。 ただし、これはそうではないことがわかります。 基本クラスのみが物理的にフィールドを持つため、
putfield
オペランドは必要なフィールドを正確に示し、その結果、変更後もコードは機能し続けます。
そして、仕様は何と言っていますか?
仕様では、章全体がバイナリ互換性のために予約されており、基本概念は「バイナリ互換」または「安全な」変更です。 仕様では、安全な変更のみが行われた場合、アプリケーションは他のすべてのエラーやリンクエラーを再コンパイルせずに安全に実行されることが保証されています。 奇妙なことに、しかし巨大な章全体では、バイナリ互換の操作の正確な定義はありませんが、例がたくさんあります:
- 既存のメソッド、コンストラクター、または初期化ブロックの実装を変更する
- 新しいフィールド、メソッド、およびコンストラクターを既存のクラスおよびインターフェースに追加する
- プライベートフィールド、メソッド、およびクラスコンストラクターの削除
- メソッドをクラス階層の上に移動する
- 新しいクラスとインターフェースを追加する
おそらく、バイナリ互換操作の定義が非常に弱いために問題が発生する可能性があります。
軟膏で飛ぶ
判明したように、仕様は十分に厳密ではなく、安全な変更がリンクのエラーにつながる場合があります。 インターフェイス、このインターフェイスを実装するクラス、およびそれらを使用する別のクラスを考えてみましょう。 public interface A {} public class B implements A {} public class CompatibilityChecker { public static void main(String[] args) { A b = new B(); } }
安全な2つの変更を行います
foo
メソッドをインターフェイス
A
追加し、
CompatibilityChecker
クラスの
main
メソッドの実装を変更します。
public interface A { void foo(); } public class CompatibilityChecker { public static void main(String[] args) { A b = new B(); b.foo(); } }
起動時に、理解できるように、エラーが発生します。つまり、
AbstractMethodError: B.foo()V
です。 この問題は既知であり、Javaバイトコード処理の中核にあります。 状況を修正する提案がありましたが、今のところ何ももたらされていません。
終わり
そのため、記事の冒頭で構成された質問に対する答え(「誰かがアプリケーションに間違ったバージョンのライブラリを置いた場合はどうなりますか?」)は次のとおりです。 コンパイル時に使用されたバージョンによって異なり、ランタイムで使用されるバージョンとは異なります。
この記事はいくつかの明らかなことを扱っていません。 たとえば、メソッドやクラスの削除やクラスのインターフェイス化などの
IncompatibleClassChangeError
があると、
NoSuchMethodError
、
NoClassDefFoundError
、
IncompatibleClassChangeError
などの容赦ないエラーが
NoClassDefFoundError
ます。
私は質問に答えて、コメントと追加を読んでうれしいです。 ちなみに、これはHabrahabrでの2度目の人生の最初のトピックです。 何を言っているのかさえ分かりません。