重写的实现和动态分派(Dispatch)有紧密的联系,分派的大概意思是,指定调用多个实现中的某个实现。
动态分派#
比如说
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();
}
}
当然,我们完全知道,会输出
son
Daughter
但是,在表现类型为 Father 的情况下,他是怎么知道 son 对应的 print 是输出son
的那个呢?
javap 输出字节码:
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 实在 new 那两个对象,和初始化,然后把应用存在 local 变量里边:[son,doughter]
然后,先拿出变量位置为1
的对象引用 (son),放到栈顶,调用invokevirtual
,#6
是个方法,invokevirtual 指令自动找到当前对象里
叫DynamicDispatch$Father.print:()V
的方法,然后调用,也就是调用了son.print();
invokevirtual#
整个过程的关键是invokevirtual
,invokevirtual 是如何查找对应的方法的呢?
- 找到操作数栈栈顶元素,拿到该元素所指的对象类型 C
- 在 C 中寻找符合要求的方法,如果找到了,就进行访问权限校验,通过了就进行返回,不通过抛
IllegalAccessError
- 如果没找到,根据继承关系,向父类进行查找
- 最后还是没找到就抛
AbstractMethodError
那为什么字节码是先aload_1
的呢?因为字节码是编译后就确定的了,所以只能是编译器这边做的工作,大概是类型推断之类的(不确定)
运行时进行确定实际类型、确定执行的方法版本的过程叫动态分派。
这就是 jvm 对动态分派的处理,也就是实现了 java 的重写,程序员借助重写编写多态程序。
静态分派#
静态分派就没这么复杂,对应的是方法重载,(有人说方法重载不算多态的表现形式,因为编译完就确定了要使用哪一个方法)
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);
}
}
方法重载调用的是尽可能相近的方法,比如假如没有print(char)
而调用print('a')
的时候,会给你调用print(int)
上边的代码的字节码是这样的:
{
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
}
编译完了就给你排好要调用哪个方法了。
单分派、多分派#
宗量, 个人理解是方法的接收者和方法的参数叫方法的宗量
- 根据宗量的大小可以区分是单分派还是多分派
比如一下代码,在分派的时候有 doRead 两个宗量 (参数不同)
public class Book{
public void read(){}
public static void doRead(Book b){
b.read();
}
public static void doRead(Picture p){
p.read();
}
}
所以说 java 是静态多分派的语言,而在动态分派的时候总是在找栈顶引用对象里的那一个方法(也就是说参数不变的),所以动态分派的时候是
单分派的。Java 是一门静态多分派,动态单分派的语言