例だけでなく、バ​​イナリ互換性

おそらくあなたの多くは、 「誰かが私のアプリケーションに間違ったバージョンのライブラリを置いたらどうなりますか?」 質問は良いのですが、このトピックでその答えと他のいくつかを見つけるでしょう。 シードタスクの場合:2つのインターフェイスと、そのうちの1つを実装するクラスがあるとします。

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破損することを想定できNoSuchMethodErrorfoo(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そのままにして、 BC変更して、 fooメソッドの再定義を完全に削除します。

 public class B extends A {} public class C extends A {} 

コードが実行されると、ダイナミックディスパッチはA.fooエントリを1つだけ検出するため、すべてのケースでそれを呼び出します。その結果、コンソールには文字「A」のみが表示され、例外は完全にA.fooます。

BCメソッドを再定義することにより、研究を続けています。 開始後、予想どおり、動的ディスパッチは仮想テーブル内のすべてのレコードを検出し、すべてを再コンパイルして得られるものとまったく同じ出力を提供します。

不適切なタイプのフィールド

以前、メソッドのみで実験を試みました。 次に、フィールドに何が起こるかを見てみましょう。 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があると、 NoSuchMethodErrorNoClassDefFoundErrorIncompatibleClassChangeErrorなどの容赦ないエラーがNoClassDefFoundErrorます。

私は質問に答えて、コメントと追加を読んでうれしいです。 ちなみに、これはHabrahabrでの2度目の人生の最初のトピックです。 何を言っているのかさえ分かりません。

Source: https://habr.com/ru/post/J133907/


All Articles