📜 ⬆️ ⬇️

Binary compatibility in the examples and not only

Perhaps many of you have asked questions like "What would happen if someone puts the wrong version of the library to my application?" . The question is good, and you will find the answer to it and some others in this topic. For the seed task: let there are two interfaces and a class that implements one of them:

public interface A { //... } public interface B { //... } public class C implements A { //... } 

And also a class in which there is a method foo , overloaded for A and B This method is called from an instance of class 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())); } } 

It is quite obvious that "A" will be displayed. It is equally obvious that if you say that C implements A, B , you get a compilation error ( for those who are not obvious to the latter, I can recommend reading about how the methods are chosen. For example, in the standard in section 15.12.2 or more just describing the ground ).
But what will happen if we recompile only C.java , and then run the CompatibilityChecker from the existing class file, is already a more complicated issue. Interested? I ask under the cat!
')

Static dispatch

Those who know that the overloaded methods are selected at compile time can figure out that for this reason the class file will immediately contain information about what method to call, and therefore the result will be “A”. Let's check this assumption:

 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 

Indeed, as can be seen from the instructions indented 19, there is a call for a very specific method. However, those who heard about the verifier may argue and suggest that he will notice that these are some wrong bees class C has changed, and throw an exception. Fortunately, they are mistaken, because the verifier checks only the correctness of the structure of classes and interfaces, and not the correspondence of the class-file versions.

So, let's run our code and make sure that the initial assumption was correct: a real "A" will be displayed.

In addition, it can be assumed that there may be some incorrect address in the virtual address table, and therefore everything will break in NoSuchMethodError with NoSuchMethodError . This assumption is also erroneous, since the foo(A) method is called, and in a virtual table it is such one. It’s another thing if there were heirs who redefine it ...

Dynamic dispatch

Suppose we have the following three classes:

 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"; } } 

And the class that calls foo in different ways:

 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()); } } 

Everyone, of course, knows that since methods of redefined types are selected in runtime, the initial output will be:
 A B B AC AC 

It's time to make a dirty trick, replacing the class file from A with the result of compiling the following code:

 public class A { public String foo(Object dummy) { return "A"; } } 

To understand what will happen in this case is quite simple. First, all attempts to call methods, where foo is called on an instance of class A , will definitely crash with NoSuchMethodError . Among these attempts is also the call super.foo() in class C Secondly, as we have seen before, the B.foo() method B.foo() called successfully.

Now let's change the tactics: A.foo make A.foo again the way it was, but now let's change B and C , completely removing the foo method override from them:

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

When you run the code, dynamic dispatch will detect only one entry for A.foo , and therefore will cause it in all cases, with the result that we will see only the letters “A” in the console and the complete absence of any exceptions.

We continue our research by redefining the methods in B and C again. After launch, as we can expect, dynamic dispatch will find all the entries in the virtual table, and will give exactly the same output as the one we would have received, recompiling everything.

Fields of unsuitable types

Earlier we tried to experiment only with methods. Now let's see what happens with the fields. Let there be a class that stores the value of type int and a successor of this class:

 public class A { int answer; } public class B extends A {} 

And, traditionally, a class B consumer:

 public class CompatibilityChecker { public static void main(String[] args) { B b = new B(); b.answer = 42; } } 

We now add to class B our own field with the same name:

 public class B extends A { String answer; } 

Let's see which byte code was generated for 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 

This listing can be confusing, since on the indent 11 in the commentary it seems that it is said that the field belongs to B. Therefore, it is worth believing that when recompiling B we will encounter an error. However, it turns out that this is not the case at all. Since physically only the base class has a field, the operand of the putfield indicates exactly that field, with the result that the code continues to work after the changes.

And what does the specification say?


In the specification, a whole chapter is devoted to binary compatibility, in which the basic concept is “binary compatible” or “safe” change. The specification states that making only safe changes ensures safe execution of the application without recompiling the rest and linking errors. Strangely enough, but in the whole huge chapter there is no exact definition of a binary compatible operation, however there are a lot of examples:
Perhaps it is because of such a weak definition of binary compatible operations that problems arise.

A spoon of tar

As it turned out, the specification is not strict enough, and you can think of a case in which safe changes will lead to an error in the link. Consider an interface, a class that implements this interface, and another class that uses them:

 public interface A {} public class B implements A {} public class CompatibilityChecker { public static void main(String[] args) { A b = new B(); } } 

Make two safe changes: add the foo method to interface A and change the implementation of the main method of the main class:

 public interface A { void foo(); } public class CompatibilityChecker { public static void main(String[] args) { A b = new B(); b.foo(); } } 

When you start, as you could understand, an error will occur, namely AbstractMethodError: B.foo()V , which in theory should not be. This problem is known and underlies the processing of Java bytecode. There were proposals to remedy the situation, but they still have not led to anything.

the end


So, the answer to the question that defended at the very beginning of the article (“And what will happen if someone fits the wrong version of the library to my application?”) Like this: “And who knows? Looking at what the version that was used during the compilation is different from that used in runtime. ”

The article does not address some obvious things. For example, that with incompatible changes like deleting methods and classes or turning a class into an interface, merciless errors like NoSuchMethodError , NoClassDefFoundError or IncompatibleClassChangeError will be NoClassDefFoundError .

I will be glad to answer questions and read comments and additions. By the way, this is my first topic in the second life on Habrahabr. I do not even know what I'm saying.

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


All Articles