Objective
As we all know, there are several major features of a Face object: encapsulation, inheritance, polymorphism.
In fact, in the interview process is often asked this question. So the question is, how does a Java Virtual machine implement polymorphism?
In fact, in the Java Virtual machine, when it comes to polymorphic implementations, we have to say that the method is called.
Method Invocation Concepts
The method invocation is not equal to the method execution, and the only task in the method invocation phase is to determine the version of the called method (which is actually called). As we all know, the class file compilation process does not include the C language compilation of the connection steps, all method calls in the class file is a symbolic reference, not the Java Runtime's entry address (here also confirms the previous article inside the Java Virtual machine and other loading mechanism to do the work). In fact, it is precisely because of this, it brings the powerful extension function to Java.
But when will the direct reference to the method's target method be determined?
In fact, the answer to this question has to be run through during class loading.
Analytical
In the previous blog, in the parsing phase of class loading, some symbolic references are converted to direct references, and part of the parsing phase "symbolic references" must meet the following criteria:
- The method has a definite version before the program actually runs.
- The invocation version of this method is immutable during runtime.
When both methods are satisfied, the symbolic reference to the method is converted to a direct reference in the parsing phase of the class load.
In fact, there are two main ways to satisfy these two requirements:
1、静态方法2、私有方法
In fact, the two methods have one thing in common, that is, they can not be rewritten at the time of inheritance, so they can be identified as a unique version in the parsing phase of class loading.
Corresponding to the above, in the Java virtual machine, there are 5 method call bytecode commands, respectively:
- Invokestatic calling a static method
- Invokespecial calls the constructor method, the private method, and the parent class method.
- Invokevirtual Calling virtual methods
- Invokeinterface invokes an interface method that determines the object that implements the interface at run time
- Invokedynamic This is a dynamic parsing and re-invocation method. It's a little complicated, let's just ignore it
From the above summary, the methods we find invokestatic and invokespecial modify seem to be uniquely determined. So they can all only determine the version when the class is loaded, meaning they can be parsed when the class loads.
We all know that static methods cannot be inherited, so let's take a look at the chestnuts below:
publicclass Main { publicstaticvoidgo(){ System.out.println("go"); } publicstaticvoidmain(String[] args) { Main.go(); }}
Using JAVAP to look at the bytecode, because the bytecode is a bit long, I intercepted some important code:
The focus is on the white box code,invokestatic #30
Let's see what #30 is:
The original is the Main.go(); program inside directly call the Main.go () method, the Visible Go () method is the only certainty.
In fact, in addition to Java inside invokestatic and invokespecial , in fact, there is a situation can be in the Java class loading parsing phase is uniquely determined, that is the final modification method. Because the final modified method cannot be overridden, the receiver of the method does not make polymorphic selections, so it can be uniquely determined at the time of parsing.
Since parsing is a static process, it can be determined during compilation that the symbolic reference is converted directly to a direct reference when parsing.
In fact, in Java, there is also a call (dispatch call), which can be static or dynamic. In fact, in this case, the implementation of the Java polymorphic Nature will be embodied.
Dispatch call
In fact, the allocation is divided into two kinds, namely dynamic allocation and static allocation. When we learn about allocations, we usually combine them with overrides and overloads.
Overloading (overload) vs. Static dispatch
Let's look at a question first:
Public class Main { Static Abstract class Father {}Static class Son extends Father {}Static class daughter extends Father {} Public voidGetsex (daughter daughter) {System.out.println ("I am a Girl"); } Public voidGetsex (son son) {System.out.println ("I am a Boy"); } Public voidGetsex (Father son) {System.out.println ("I am a father"); } Public Static voidMain (string[] args) {Father son =NewSon (); Father daughter =NewDaughter (); Main main =NewMain (); Main.getsex (son); Main.getsex (daughter); }}
You can guess what you're going to get out of your experience.
In fact, this chestnut embodies the heavy load.
If we change it in the code:
main.getSex((Son)son);
It will outputi am a boy
In fact, here also reflects the static allocation of Java, we can see that the main object has been confirmed, then main in the run- main.getSex(son); time selection method, the choice getSex(Son son) or not getSex(Father son) ?
The reference type of son in our code is Father , but its actual type is Son .
Let's take a look at the resulting bytecode:
Byte code inside 0-23 we skip directly, because 0-23 pairs of codes are
new Son(); new Daughter(); new Main();
The function of the bytecode here is to create memory space, and then put the son, daughter and main instances into the 1th, 2, 3 instance variable table slot, here actually there is a No. 0 instance, this pointer, placed in the No. 0 slot, this is beyond the content of this article to explain, so skip.
We see from 24 that aload_x is putting the instance you just created into the operand stack before it can be manipulated. You can see the following line 26th:
#50//Method getSex:(LMain$Father;)
As you can see here, it has been determined in bytecode that the receiver of the method is the main and the version of the method getSex(Father father) , so we will output it when we run the code i am a father .
In fact, the Java compiler uses the static type of the parameter instead of the actual type to determine which overloaded version to use when overloading. So here in the bytecode, we have chosen getSex(Father father) to call the target and write the symbolic reference of the method into the parameters of several instructions in the main method invokevirtual .
Therefore, the dispatch action of the version that relies on the static type to locate the execution of the method becomes a static dispatch. A typical application for static dispatch is method overloading, and static dispatch occurs during compilation, so statically dispatched actions are emitted by the compiler.
In addition, the compiler can determine the overloaded version of the method, but in many cases, this version is not necessarily unique, for example, I changed the above code:
Public class Main { Static Abstract class Father {}Static class Son extends Father {} Public voidGetsex (son son) {System.out.println ("I am a Boy"); } Public voidGetsex (Father son) {System.out.println ("I am a father"); } Public Static voidMain (string[] args) {son son =NewSon (); Main main =NewMain (); Main.getsex (son); }}
and then output:
This is a very normal execution result, and if we comment out getsex son, then run the test again:
found that the compiler did not find getSex(Son son) this method, only to make the appropriate compromises, the son up to father, and then chose the getSex(Fatherson) method.
If we getSex(Fatherson) drop the comment again, we'll find:
Here again the compromise is chosen and the transition upward continues into object.
To sum up: Static allocation is the most appropriate version of a method to overload, but this version is not the only one determined. When we write code, we should try to avoid this, although it seems to show that you are knowledgeable, but this is not a wise choice.
Override (override) vs. dynamic dispatch
After reading the static assignment, let's take a look at the dynamic dispatch. Dynamic dispatch is often closely associated with rewriting, so let's look at a rewrite of chestnuts first:
Public class Main { Static class Father { Public voidSay () {System.out.println ("I am fasther"); } }Static class Son extends Father {@Override Public voidSay () {System.out.println ("I am Son"); } }Static class daughterextends Father { @Override Public voidSay () {System.out.println ("I am Daughter"); } } Public Static voidMain (string[] args) {Father son =NewSon (); Father daughter =NewDaughter (); Son.say (); Daughter.say (); }}output:i am Soni am Daughter
I believe everyone knows what the output is, three classes have the Say () method, but how does the virtual machine know which method to call? Don't worry, we're still in the habit of looking at byte codes:
Now I believe you can probably understand what's going on inside the bytecode?
We found that the Java code corresponding to lines 17th and 21st should be:
son.say(); daughter.say()
In terms of bytecode, the two lines of code are the same. Call the same method of the same class, all Father.say() , so why is their final output different??
The reason for this is to invokevirtual start with polymorphic lookups, the invokevirtual parsing process of the instruction runtime is probably as follows:
- Finds the actual type of the object pointed to by the stack top element of the operand stack, recorded as C
- If you find a method in type C that matches both the descriptor and the simple name, the access check is performed. Pass back the direct reference to this method, otherwise return
illegalaccesserror .
- Otherwise, follow the inheritance relationship from next to the parent class of C to find step 2.
- If the appropriate method is never found, run out of the
AbstractMethodError exception.
Since the instruction is looking for the invokevirtual actual type of receiver at runtime at the first step of execution, the instructions above two calls invokevirtual can successfully find the actual type of say () method, and then parse the symbolic reference of the class method to a different direct reference, which is also the embodiment of the rewrite.
Then this kind of running period according to the actual type to judge the method of the execution version of the dispatch process called dynamic allocation.
For overrides and overloads of the first end, but also to fill in the rest of the allocation of other content, welcome attention.
Method invocation of the Java Virtual Machine (top)