Java面向对象的三个基本特征:继承,封装,多态,我们这次来看Java虚拟机如何实现,这里说的不是语法上的实现。
静态分派:
在Java语言中符合“编译期可知,运行期不可变”这个要求的方法,主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了他们不可能通过继承或别的方式重写其他版本,因此他们适合在类加载阶段进行解析。
静态方法、私有方法、实例构造器、父类方法。这些方法称为非虚方法,它们在类加载的时候就会把符号引用解析为该方法的直接引用。与之相反,其他方法称为虚方法(除去final方法)。
public class StaticDiptch {
/*
* 方法静态分派演示
*/
static abstract class Human{
}
static class Man extends Human{
}
static class Woman extends Human{
}
public static void sayHello(Human guy){
System.out.println("hello,guy!");
}
public static void sayHello(Man guy){
System.out.println("hello,gentlemen!");
}
public static void sayHello(Woman guy){
System.out.println("hello,lady!");
}
public static void main(String[] args) {
StaticDiptch at= new StaticDiptch();
Human man=new Man();
Human woman=new Woman();
at.sayHello(man);
at.sayHello(woman);
}
}
运行结果:
hello,guy!
hello,guy!
解释两个概念(重要嘞):
Human man=new Man();
我们把“Human”称为变量的静态类型,后面的“Man”称为变量的实际类型,静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型在编译器可知;而实际类型变化的结果在运行期才确定,编译器在编译期并不知道一个对象的实际类型是什么。
在接受者都为at的情况下,重载的版本完全取决于传入的形参,但虚拟机在编译器的编译时期传入的参数,是通过静态类型决定的也就是Human决定,
俺们来看字节码指令:
俺只拿了main方法的和常量池类
Constant pool:
#1 = Class #2 // diptch/StaticDiptch
#2 = Utf8 diptch/StaticDiptch
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Ldiptch/StaticDiptch;
#14 = Utf8 sayHello
#15 = Utf8 (Ldiptch/StaticDiptch$Human;)V
#16 = Fieldref #17.#19 // java/lang/System.out:Ljava/io/Prin
tStream;
#17 = Class #18 // java/lang/System
#18 = Utf8 java/lang/System
#19 = NameAndType #20:#21 // out:Ljava/io/PrintStream;
#20 = Utf8 out
#21 = Utf8 Ljava/io/PrintStream;
#22 = String #23 // hello,guy!
#23 = Utf8 hello,guy!
#24 = Methodref #25.#27 // java/io/PrintStream.println:(Ljava
/lang/String;)V
#25 = Class #26 // java/io/PrintStream
#26 = Utf8 java/io/PrintStream
#27 = NameAndType #28:#29 // println:(Ljava/lang/String;)V
#28 = Utf8 println
#29 = Utf8 (Ljava/lang/String;)V
#30 = Utf8 guy
#31 = Utf8 Ldiptch/StaticDiptch$Human;
#32 = Utf8 (Ldiptch/StaticDiptch$Man;)V
#33 = String #34 // hello,gentlemen!
#34 = Utf8 hello,gentlemen!
#35 = Utf8 Ldiptch/StaticDiptch$Man;
#36 = Utf8 (Ldiptch/StaticDiptch$Woman;)V
#37 = String #38 // hello,lady!
#38 = Utf8 hello,lady!
#39 = Utf8 Ldiptch/StaticDiptch$Woman;
#40 = Utf8 main
#41 = Utf8 ([Ljava/lang/String;)V
#42 = Methodref #1.#9 // diptch/StaticDiptch."<init>":()V
#43 = Class #44 // diptch/StaticDiptch$Man
#44 = Utf8 diptch/StaticDiptch$Man
#45 = Methodref #43.#9 // diptch/StaticDiptch$Man."<init>":(
)V
#46 = Class #47 // diptch/StaticDiptch$Woman
#47 = Utf8 diptch/StaticDiptch$Woman
#48 = Methodref #46.#9 // diptch/StaticDiptch$Woman."<init>"
:()V
#49 = Methodref #1.#50 // diptch/StaticDiptch.sayHello:(Ldip
tch/StaticDiptch$Human;)V
#50 = NameAndType #14:#15 // sayHello:(Ldiptch/StaticDiptch$Hum
an;)V
#51 = Utf8 args
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #1 // class diptch/StaticDiptch
3: dup
4: invokespecial #42 // Method "<init>":()V
7: astore_1
8: new #43 // class diptch/StaticDiptch$Man
11: dup
12: invokespecial #45 // Method diptch/StaticDiptch$Man.
"<init>":()V
15: astore_2
16: new #46 // class diptch/StaticDiptch$Woman
19: dup
20: invokespecial #48 // Method diptch/StaticDiptch$Woma
n."<init>":()V
23: astore_3
24: aload_2
25: invokestatic #49 // Method sayHello:(Ldiptch/Static
Diptch$Human;)V
28: aload_3
29: invokestatic #49 // Method sayHello:(Ldiptch/Static
Diptch$Human;)V
32: return
LineNumberTable:
line 26: 0
俺们只看24-29行,分别在加载了局部变量表中索引为2,和索引为3的的引用也就是我们指向man,women的索引。,25和29行分别执行的是sayHello方法指向了俺们常量池索引为49滴位置,也就是父类Human的sayHello(),所以嘞。结果就一米了然嘞。
编译器在重载时是通过参数的静态类型而不是实际类型作为判定的依据。并且静态类型在编译期可知,因此,编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本。
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用就是方法重载。
静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的,而是由编译器来完成。
但是,字面量没有显示的静态类型,它的静态类型只能通过语言上的规则去理解和推断。