The implementation of overriding is closely related to dynamic dispatch (Dispatch), which generally means specifying the invocation of a certain implementation among multiple implementations.
Dynamic Dispatch#
For example:
public class DynamicDispatch {
static abstract class Father {
public abstract void print();
}
static class Son extends Father {
public void print() {
System.out.println("son");
}
}
static class Daughter extends Father {
public void print() {
System.out.println("Daughter");
}
}
public static void main(String[] args) {
Father son = new Son();
Father dou = new Daughter();
son.print();
dou.print();
}
}
Of course, we know that it will output:
son
Daughter
However, in the case where the type is Father, how does it know that the print corresponding to son outputs son
?
The javap
command outputs the bytecode:
Classfile /D:/tech/java/vmdemo/src/main/java/DynamicDispatch.class
Last modified 2019-1-10; size 499 bytes
MD5 checksum a5660428b8d1d4ed7fb4f04bd86f7738
Compiled from "DynamicDispatch.java"
public class DynamicDispatch
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#22 // java/lang/Object."<init>":()V
#2 = Class #23 // DynamicDispatch$Son
#3 = Methodref #2.#22 // DynamicDispatch$Son."<init>":()V
#4 = Class #24 // DynamicDispatch$Daughter
#5 = Methodref #4.#22 // DynamicDispatch$Daughter."<init>":()V
#6 = Methodref #12.#25 // DynamicDispatch$Father.print:()V
#7 = Class #26 // DynamicDispatch
#8 = Class #27 // java/lang/Object
#9 = Utf8 Daughter
#10 = Utf8 InnerClasses
#11 = Utf8 Son
#12 = Class #28 // DynamicDispatch$Father
#13 = Utf8 Father
#14 = Utf8 <init>
#15 = Utf8 ()V
#16 = Utf8 Code
#17 = Utf8 LineNumberTable
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 SourceFile
#21 = Utf8 DynamicDispatch.java
#22 = NameAndType #14:#15 // "<init>":()V
#23 = Utf8 DynamicDispatch$Son
#24 = Utf8 DynamicDispatch$Daughter
#25 = NameAndType #29:#15 // print:()V
#26 = Utf8 DynamicDispatch
#27 = Utf8 java/lang/Object
#28 = Utf8 DynamicDispatch$Father
#29 = Utf8 print
{
public DynamicDispatch();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class DynamicDispatch$Son
3: dup
4: invokespecial #3 // Method DynamicDispatch$Son."<init>":()V
7: astore_1
8: new #4 // class DynamicDispatch$Daughter
11: dup
12: invokespecial #5 // Method DynamicDispatch$Daughter."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method DynamicDispatch$Father.print:()V
20: aload_2
21: invokevirtual #6 // Method DynamicDispatch$Father.print:()V
24: return
LineNumberTable:
line 24: 0
line 25: 8
line 27: 16
line 28: 20
line 29: 24
}
SourceFile: "DynamicDispatch.java"
InnerClasses:
static #9= #4 of #7; //Daughter=class DynamicDispatch$Daughter of class DynamicDispatch
static #11= #2 of #7; //Son=class DynamicDispatch$Son of class DynamicDispatch
static abstract #13= #12 of #7; //Father=class DynamicDispatch$Father of class DynamicDispatch
0-15 are for creating those two objects and initializing them, then storing the application in local variables: [son,daughter]
Then, it first takes the object reference at position 1
(son) and puts it on the top of the stack, calling invokevirtual
, #6
is a method, and the invokevirtual
instruction automatically finds the method named DynamicDispatch$Father.print:()V
in the current object, and then calls it, which is son.print();
invokevirtual#
The key to the whole process is invokevirtual
. How does invokevirtual
find the corresponding method?
- Find the top element of the operand stack and get the type C of the object it points to.
- Look for the required method in C. If found, perform access permission checks; if passed, return; if not, throw
IllegalAccessError
. - If not found, search for the method in the parent class based on the inheritance relationship.
- If still not found, throw
AbstractMethodError
.
So why is the bytecode first aload_1
? Because the bytecode is determined after compilation, so it can only be the work done by the compiler, which is likely type inference (not certain).
The process of determining the actual type and the version of the method to execute at runtime is called dynamic dispatch.
This is how the JVM handles dynamic dispatch, which implements Java's overriding, allowing programmers to write polymorphic programs through overriding.
Static Dispatch#
Static dispatch is not so complicated; it corresponds to method overloading (some say method overloading is not a form of polymorphism because it is determined which method to use after compilation).
public class StaticDispatch {
static void print(int i){
System.out.println("int!");
}
static void print(char i){
System.out.println("char!");
}
static void print(boolean i){
System.out.println("bool!");
}
public static void main(String[] args) {
print(1);
print('1');
print(true);
}
}
Method overloading calls the closest method possible. For example, if there is no print(char)
and you call print('a')
, it will call print(int)
instead.
The bytecode for the above code is as follows:
{
public StaticDispatch();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
static void print(int);
descriptor: (I)V
flags: ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String int!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 8: 0
line 9: 8
static void print(char);
descriptor: (C)V
flags: ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String char!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 12: 0
line 13: 8
static void print(boolean);
descriptor: (Z)V
flags: ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String bool!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 16: 0
line 17: 8
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: iconst_1
1: invokestatic #7 // Method print:(I)V
4: bipush 49
6: invokestatic #8 // Method print:(C)V
9: iconst_1
10: invokestatic #9 // Method print:(Z)V
13: return
LineNumberTable:
line 20: 0
line 22: 4
line 24: 9
line 25: 13
}
After compilation, it arranges which method to call.
Single Dispatch and Multiple Dispatch#
Arity, my personal understanding is that the receiver of the method and the parameters of the method are called the arity of the method.
- Based on the arity, it can be distinguished whether it is single dispatch or multiple dispatch.
For example, in the following code, there are two arities (different parameters) when dispatching doRead
:
public class Book{
public void read(){}
public static void doRead(Book b){
b.read();
}
public static void doRead(Picture p){
p.read();
}
}
Therefore, Java is a statically multi-dispatch language, while during dynamic dispatch, it always looks for the method in the top reference object (meaning the parameters remain unchanged), so dynamic dispatch is single dispatch. Java is a statically multi-dispatch, dynamically single-dispatch language.