I. Overview
Today's software is increasingly dependent on shared components developed by different vendors and authors, and component management is becoming increasingly important. An extremely important issue in this regard is the binary compatibility of different versions of a class, that is, when a class changes, can the new class replace the original class directly, without damaging other components that are developed by different vendors and authors that depend on that class?
The main goal of the Java Binary Compatibility concept is to promote the widespread reuse of software on the Internet, and it avoids the underlying class fragility problems faced by most C + + environments-for example, in C + +, access to a domain (data member or instance variable) is compiled into an offset relative to the object's starting position. It is determined at compile time that if the class joins the new domain and recompile, the offsets will change, and the previously compiled code that uses the old version of the class is not executed properly, and the virtual method invocation has the same problem.
The C + + environment typically solves the problem by recompiling all code that references the modified class. In Java, the same strategy applies to a small number of development environments, but there are many limitations to this strategy. For example, suppose someone developed a program p,p references an external library L1, but the author of P does not have L1 source code; L1 uses another library L2. Now that the L2 has changed, but the L1 cannot be recompiled, the development and changes of P are limited.
To this end, Java introduces the concept of binary compatibility-if the change to L2 is binary compatible, then the changed L2, the original L1, and now p can be connected smoothly without any errors.
Let's start with a simple example. The authorization and Hello classes come from two different authors, authorization provides authentication and authorization services, and the Hello class calls the authorization class.
Package Com.author1;public class Authorization {public Boolean authorized (String UserName) { return true; }}
Package Com.author2;import Com.author1.*;class Hello {public static void Main (String arg[]) { Authorization Auth = new Authorization (); if (auth.authorized ("MyName")) System.out.println ("You have passed the verification"); else System.out.println ("You failed to authenticate");} }
Now that Author1 has released version 2.0 of the authorization class, the author of the Hello class Author2 want to use the new version of the authorization class without changing the original Hello class. The 2.0 version of the authorization is much more complicated than the original:
Package Com.author1;public class Authorization {public Token authorized (String userName, string pwd) { return nu ll; } Private Boolean Determineauthorization (String userName, string pwd) { return true; } Public Boolean authorized (String UserName) { return true; } public class Token {}}
Author Author1 committed to version 2.0 of the Authorization class and 1.0 version of the class binary compatibility, or, 2.0 version of the authorization class still satisfies the 1.0 version of the authorization class and Hello Class conventions. Obviously, when Author2 compiles the Hello class, there is no error with which version of the authorization class is used-in fact, if only because the authorization class is upgraded, the Hello class does not have to be recompiled at all. The same hello.class can call any one of the authorization.class.
This feature is not unique to Java. The UNIX system has long had the concept of a Shared object library (. So file), and the Windows system also has the concept of a dynamic link library (. dll file) that can be changed to another library by simply replacing the file. Just like Java's Binary compatibility feature, the name connection is done at run time, not in the compilation, connection phase of the code, so it also has the advantages of Java binary compatibility, such as modifying the code simply by recompiling a library to make it easier to modify a part of the program. However, Java's binary compatibility has its own unique advantages:
⑴java the granularity of binary compatibility from the entire library (which may contain dozens of, hundreds of classes) to a single class.
⑵ in languages such as C + +, creating shared libraries is often a conscious behavior, and an application typically does not provide many shared libraries, which can be shared, and which code is not shareable, are pre-programmed results. But in Java, binary compatibility becomes an innate natural feature.
⑶ shared objects are only for function names, but Java binary compatibility takes into account overloads, function signatures, and return value types.
⑷java provides a more sophisticated error-control mechanism, and version incompatibility triggers an exception, but it can be easily captured and processed. In contrast, the incompatibility of shared library versions often causes serious problems in C + + +.
ii. compatibility of classes and objects
The concept of
Binary compatibility is similar to the concept of object serialization in some ways, and there is a certain overlap between the two goals. When serializing a Java object, the name of the class, the name of the domain, is written to a binary output stream, and the serialized-to-disk object can be read by different versions of the class, provided the name, domain, and type required by the class are present and consistent. The following table compares the two concepts of binary compatibility and serialization.
comparison |
object serialization |
binary compatible |
for |
|
Class |
compatibility requirements |
|
class, field, method |
Delete operation causes incompatible |
always |
not necessarily |
Compatibility after modifying access properties (Public,private, etc.) |
is |
no |
Binary compatibility and serialization take into account the constantly updated version of the class, allowing methods and domains to be added to the class, and the pure addition does not affect the semantics of the program; Similarly, simple structural modifications, such as rearranging fields or methods, do not cause any problems.
third, delay binding
The key to understanding binary compatibility is to understand deferred binding (late binding). Deferred binding means that Java does not check the name of a class, domain, or method until run time, rather than having the name of the class, domain, method, and instead of the names of the classes, fields, and methods in the compilation, and substituting the offset values-this is the key to the role of Java binary compatibility.
Because of the delayed binding technique, the name of the method, domain, and class is resolved until run time, which means that the body of the class can be arbitrarily replaced as long as the name (and type) of the domain, method, and so on, which is, of course, a simplification, and other rules that govern the binary compatibility of Java classes. For example, access attributes (private, public, and so on) and whether abstract (if a method is abstract, it is definitely not called directly), etc., but the delay binding mechanism is undoubtedly the core of binary compatibility.
Only by mastering the rules of binary compatibility can you ensure that other classes are not affected when you rewrite the class. Let's look at an example below, Frodomail and Sammail are two email programs:
Interface Classifiable { boolean isjunk ();} Abstract class Message implements classifiable {}class Emailmessage extends Message {public boolean isjunk () {retur n false; }}class frodomail {public static void Main (String a[]) { classifiable m = new Emailmessage (); System.out.println (M.isjunk ());} } Class Sammail {public static void Main (String a[]) { emailmessage m = new Emailmessage (); System.out.println (M.isjunk ());} }
If we re-implement the message and no longer allow it to implement the Classifiable interface, Sammail will still work, but Frodomail throws an exception: Java.lang.IncompatibleClassChangeError at Frodomail.main. This is because Sammail does not require emailmessage to be a classifiable, but Frodomail requires emailmessage to be a classifiable, Compile frodomail the resulting binary. class file references the Classifiable interface name. The method conforming to the Classifiable interface definition still exists, but the class does not mention the Classifiable interface at all.
iv. Rules of Compatibility: Methods
From the point of view of binary compatibility, a method consists of four parts: the name of the method, the return value type, the parameter, and whether the method is static. Changing any of these four projects has become another method for the JVM.
Take the "Boolean IsValid ()" method as an example, if you let IsValid receive a date parameter and become a "Boolean isValid (Date when)", the modified class cannot directly replace the original class, attempting to access the new class's IsValid () Method can only get an error message similar to the following: Java.lang.NoSuchMethodError:Ticket.isValid () Z. The JVM uses the notation "() Z" to indicate that the method does not accept parameters and returns a Boolean. With regard to this issue, the following will be described in more detail.
The JVM uses a technique called virtual method Dispatch to determine the method body to invoke, which determines the method body to use based on the actual instance in which the method is called, and can be seen as an extended deferred binding strategy.
If the class does not provide a method that exactly matches the name, parameter, and return value type, it uses the method inherited from the superclass. Because of Java's binary compatibility rules, this inheritance is actually determined during run time, not during compilation. Suppose there are several classes:
Class Poem { void perform () { System.out.println ("Day depends on Mountain");} } Class Shakespearepoem extends Poem { void perform () { System.out.println ("To is or not to IS.");} } Class Hamlet extends Shakespearepoem {}
So:
Poem Poem = new Hamlet ();p oem.perform ();
The output "to is or not to IS.". This is because the Perform method body is determined at run time. Although Hamlet does not provide a method body for perform, it inherits one from Shakespearepoem. As for why the Perform method is not poem defined, it is because the perform defined by Shakespearepoem has already covered it. We can modify the hamlet at any time without recompiling the Shakespearepoem, as shown in the following example:
Class Hamlet extends Shakespearepoem { System.out.println ("Not even a mouse is noisy");}
Now, the previous example will output "even a mouse is not noisy." But
Poem Poem = new Shakespearepoem ();p oem.perform ();
The output of this code is "to is or not to IS." If we delete the contents of the Shakespearepoem, the same code will output "Day by Mountain".
Five, Compatibility rules: Domain
Fields and methods are different. Once a method of a class is removed, it is possible to obtain a different method with the same name and parameters through inheritance, but the domain cannot be overwritten, which makes the domain's performance in terms of binary compatibility different.
For example, suppose you have the following three classes:
Class Language { String greeting = "Hello";} Class German extends Language { String greeting = "Guten Tag";} Class French extends Language { String greeting = "Bon jour";}
The void test1() { System.out.println(new French().greeting); }
result is "Bon jour", but void test2() { System.out.println(((Language) new French()).greeting); }
the output is "hello". This is because the actual access to the domain depends on the type of the instance. In the first output example, Test1 accesses a French object, so the output is a French greeting, but in the second case, although a French object is actually accessed, the French object has been trained as a language object, So the output is language greeting.
If you change the language of the above example into the following form:
Class Language {}
Run Test2 again (without recompiling) and get the result is an error message: Java.lang.NoSuchFieldError:greeting. If Test2 is recompiled, a compilation error occurs: Cannot resolve symbol,symbol:variable greeting, Location:class Language System.out.println (( Language) New French ()). greeting);. Test1 is still functional and does not need to be recompiled because it does not require the language contained greeting variables.
Vi. in-depth understanding of delayed binding
The following classes are used to determine the drink and the temperature of the wine for dinner today.
Abstract class Wine { //Recommended wine temperature abstract float temperature ();} Class Redwine_wine extends Wine { //red wine temperature is usually slightly higher than white wine float temperature () {return 63;}} Class Whitewine_wine extends Wine { float temperature () {return 47;}} Class Bordeaux_redwine_wine extends Redwine_wine { float temperature () {return 64;}} Class Riesling_whitewine_wine extends Whitewine_wine { //inherit the temperature of the Whitewine class}
In the second invocation of example1, the only thing we can be sure of for the wine object is that it's a wine, but it can be Bordeaux, or it can be Riesling or something. In addition, we can be sure that the wine object cannot be an instance of the wine class itself, because the wine class is an abstract class. Compiling the source code, the Wine.temperature () call in the source code will become "Invokevirtual wine/temperature () F" (the class file actually contains the binary code of the text representation, This textual instruction description method is called the Oolong method, which represents a method call-a normal (virtual) method call, not a static call. The method that it calls is the temperature of the wine object, the right "() F" parameter is called the signature (signature), "() F", the empty parenthesis in this signature means that the method does not require an input parameter, and F indicates that the return value is a floating-point number.
When the JVM executes to the statement, it does not necessarily call the temperature method defined by the wine. In fact, in this case, the JVM cannot call the temperature method defined by the wine, because the temperature method is a virtual method. The JVM first checks the class to which the object belongs, looks for a method that conforms to the name, signature characteristics specified by the Invokevirtual statement, checks the superclass of the class if it is not found, and then the superclass of the superclass until an appropriate method implementation is found.
In this case, if the object actually created is a Bordeaux, the JVM calls the temperature () F defined by the Bordeaux class, and the temperature () F method returns 64. If the object is a RIESLING,JVM cannot find the appropriate method in the Riesling class, continue to find the Whitewine class, find an appropriate temperature () F method in the Whitewine class, the return value of this method is 47.
Therefore, the process of finding an available method is the process of finding a suitable method along the inheritance tree of the class through string matching. Understanding this principle helps to understand which modifications do not affect binary compatibility.
First, rearranging the methods inside the class obviously does not affect binary compatibility-which is generally not allowed in C + + programs, because C + + programs use a numeric offset rather than a name to determine which method to invoke. The key advantage of delayed binding is that if Java also uses the offset of the method in the class to determine the method to invoke, it will inevitably limit the binary compatibility mechanism's play, even if minimal changes can result in a large amount of code that needs to be recompiled.
Description: Some people might think that C + + is handled faster than Java, because finding a method based on a numerical offset is definitely faster than a string match. There is some truth to this argument, but only when the class just loaded, and then the Java JIT compiler processing is also a numeric offset, and no longer rely on string matching method to find methods, because the class loaded into memory can not be changed, so at this time the JIT compiler does not have to worry about the binary compatibility problem. So, at least at this point in the method invocation, Java has no reason to be slower than C + +.
Second, it is also important to check the inheritance of classes not only at compile time, but also in the runtime JVM to check the inheritance of classes.
Vii. overloading and covering
The most important thing to grasp through the previous example is that the method matching is based on the name of the method and the textual description of the signature. Here are some ways to add a glass to the sommelier class:
Class Sommelier { Wine recommend (String meal) { return null; } Glass Fetchglass (wine wine) {return null;} Glass Fetchglass (Redwine_wine Wine) {return null;} Glass Fetchglass (Whitewine_wine Wine) {return null;}}
Then compile the following code:
void Example2 () { Glass Glass; Wine wine = sommelier.recommend ("Duck"); if (wine instanceof bordeaux_redwine_wine) { //invokevirtual Sommelier/fetchglass (lredwine;) Lglass; Glass = Sommelier.fetchglass ((bordeaux_redwine_wine) Wine); } else { //invokevirtual Sommelier/fetchglass (lwine;) Lglass; Glass = Sommelier.fetchglass (wine); }}
Here are two Fetchglass calls: the first parameter to call is a Bordeaux object, and the second parameter to call is a wine object. The instructions generated by the Java compiler for these two lines of code are:
Invokevirtual Sommelier/fetchglass (lredwine;) lglass;invokevirtual Sommelier/fetchglass (LWine;) LGlass;
Note that the difference between the two is determined at compile time, not at runtime. The JVM uses the "l< class name >" symbol to represent a class (as in the previous example, the same as F), and the input parameter for both method calls is a wine or redwine, and the return value is a glass.
The sommelier class does not provide an input parameter is a Bordeaux method, but the input parameter of one method is Redwine, so the method signature of the first call uses the method that the input parameter is redwine. As for the second call, the compile time only knows that the parameter is a wine object, so the compiled instruction uses the input parameter as the Wine object method. For the second call, even if the sommelier recommendation is a Riesling object, the actual call will not be Fetchglass (whitewine), but Fetchglass (wine) (explained in bold section below), for the same reason, The method that is called is always a method that exactly matches the signature.
In this example, the different definitions of the Fetchglass method are overloaded (overload) relationships rather than overwrite (override) relationships, because the signatures of these fetchglass methods differ. If a method overwrites another method, the two must have the same parameter and return value type. Virtual method calls look for specific types at run time, only for overridden methods (with the same signature), rather than for overloaded methods (with different signatures). The parsing of the overloaded method is done at compile time, and the parsing of the override method is performed at run time.
If you delete Fetchglass (redwine), do not recompile, and then run EXAMPLE2,JVM, you will be prompted with an error message: Java.lang.NoSuchMethodError:Sommelier.fetchGlass (lredwine ;) Lglass;
However, after the method is removed, the compilation example2 can still pass, but at this point two Sommelier.fetchglass calls will generate the same invokevirtual instruction, namely: Invokevirtual sommelier/ Fetchglass (lwine;) Lglass;
If you put back the Fetchglass (Redwine) method again, Fetchglass (Redwine) will not be invoked unless example2 is recompiled, and the JVM uses Fetchglass (wine). When an incoming object is a riesling, it also does not use Fetchglass (whitewine) for the same reason: because a specific object cannot be determined at compile time. Therefore, a more generalized approach is chosen.
In the "Invokevirtual wine/temperature () F" directive, the JVM does not strictly persist in using the Wine object, but instead automatically looks for objects that actually implement the temperature method, but in the invokevirtual Sommelier/fetchglass (lredwine;) Lglass; " directive, the JVM cares about Redwine. What is this for? Because wine is not a method signature in the first instruction, it is used only to invoke the previous type check, whereas in the second instruction, Redwine is part of the method signature, the JVM must find the method to invoke based on the method signature and the method name.
Suppose we add a Fetchglass method to the Sommelier class:
Class Sommelier { Wine recommend (String meal) { return null; } Glass Fetchglass (wine wine) {return null;} Glass Fetchglass (Redwine_wine Wine) {return null;} Glass Fetchglass (Whitewine_wine Wine) {return null;} Class Redwineglass extends Glass { } redwineglass Fetchglass (redwine_wine Wine) {return null;}}
Then look at the original compiled example2, it used "invokevirtual Sommelier/fetchglass (lredwine;) Lglass;" Directive calls the Fetchglass method. The newly added method does not automatically work because Redwineglass and glass are two different types. However, if we recompile example2, the example of calling Bordeaux will become "Invokevirtual Sommelier/fetchglass (lredwine;) Lredwineglass;".
In summary, we can summarize the following important principles of Java Binary compatibility:
(1) At compile time, the Java compiler chooses the most matching method signature.
(2) at runtime, the JVM looks for exact matching method names and signatures. Similar names and signatures will be ignored.
(3) If the appropriate method is not found, the JVM throws an exception and does not load the specified class.
(4) Overloaded methods are processed at compile time, and the overridden methods are processed at run time.
Java Binary Compatibility principle