1、多态
多态是同一个行为具有多个不同表现形式或形态的能力。 多态就是同一个接口,使用不同的实例而执行不同操作
-
多态存在的三个必要条件:继承、重写、父类引用指向子类对象。
-
多态的体现:重写、接口、抽象类和抽象方法
-
多态性语言具有灵活,抽象,行为共享,代码共享的优势,很好的解决了应用程序函数同名问题。
2、方法解析
类从被载到虚拟机内存,到卸载出内存为止,整个生命周期如上图。那有些 符号引用转化成直接引用,是不是也发生在上面某个阶段呢?
其实就是根据 在哪个阶段 符号引用 转化成直接引用,将方法调用分成:解析调用 与 分派调用。
在类加载的解析阶段,会将一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来,这类方法的处理称为解析。
对于方法的调用,虚拟机提供了四条方法调用的字节码指令,分别是:
-
invokestatic: 调用静态方法
-
invokespecial: 调用构造方法,私有方法,父类方法
-
invokevirtual: 调用虚方法
-
invokeinterface: 调用接口方法
只要能被 invokestatic 和 invokespecial 指令调用的方法,都可以在解析阶段中确定唯一的调用版本,因此在类加载阶段就能找到方法块的内存地址进行执行(编译时确定,并且存在方法调用的入口),符合这个条件的方法有:静态方法、私有方法、实例构造器、父类方法 4类。而invokevirtual和invokeinterface则是运行期间动态绑定方法的直接引用。final方法也是通过invokevirtual字节码调用。
3、分派
-
解析调用一定是个静态过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用转化为可确定的直接引用,不会延迟到运行期再去完成。
-
分派调用则可能是静态的也可能是动态的,根据分派依据的宗量数(方法的调用者和方法的参数统称为方法的宗量)又可分为单分派和多分派。两类分派方式两两组合便构成了静态单分派、静态多分派、动态单分派、动态多分派四种分派情况。
3.1、静态分派
所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派,静态分派的最典型应用就是多态性中的方法重载。静态分派发生在编译阶段,因此确定静态分配的动作实际上不是由虚拟机来执行的。下面通过一段方法重载的示例程序来更清晰地说明这种分派机制:
public class Static {
static class Animal{
}
static class Duck extends Animal{
}
static class Cat extends Animal{
}
public void foo(Animal a){
System.out.println("Animal foo");
}
public void foo(Duck d){
System.out.println("gagaga");
}
public void foo(Cat c){
System.out.println("miaomiaomiao");
}
public static void main(String[] args){
Animal d = new Duck();
Animal c = new Cat();
Static s = new Static();
s.foo(d);
s.foo(c);
}
}
输出:
Animal foo
Animal foo
在变量的声明中,Animal是变量的静态类型或外观类型,编译期就能确定的,而Duck和Cat是运行类型或着说实际类型,在运行期才能确定,由此可知编译器会根据传入变量的静态类型确定调用的方法。
3.2、动态分派
动态分派与多态性的另一个重要体现——方法覆写(重写)有着很紧密的关系。向上转型后调用子类覆写的方法便是一个很好地说明动态分派的例子。这种情况很常见,因此这里不再用示例程序进行分析。很显然,在判断执行父类中的方法还是子类中覆盖的方法时,如果用静态类型来判断,那么无论怎么进行向上转型,都只会调用父类中的方法,但实际情况是,根据对父类实例化的子类的不同,调用的是不同子类中覆写的方法,很明显,这里是要根据变量的实际类型来分派方法的执行版本的。而实际类型的确定需要在程序运行时才能确定下来,这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
class Drink{}
class Eat{}
class Animal{
public void doSomething(Drink d){
System.out.println("Animal drink");
}
public void doSomething(Eat e){
System.out.println("Animal eat");
}
}
class Duck extends Animal{
@Override
public void doSomething(Drink d){
System.out.println("Duck drink");
}
@Override
public void doSomething(Eat e){
System.out.println("Duck eat");
}
}
public class Dynamic {
public static void main(String[] args) {
Animal a = new Animal();
Duck d = new Duck();
a.doSomething(new Eat());
d.doSomething(new Drink());
}
}
输出:
Animal eat
Duck drink
总结
1、静态分派
我们首先来看编译阶段编译器的选择过程,即静态分派过程。这时候选择目标方法的依据有两点:一是方法的接收者的静态类型是 Animal还是 Duck,静态类型是编译期可知的,二是方法参数类型是 Eat 还是 Drink,参数类型也是根据静态类型确定的。因为是根据两个宗量进行选择,所以 Java 语言的静态分派属于多分派类型,方法重载是常见的静态分派。
2、动态分派
再来看运行阶段虚拟机的选择,即动态分派过程。由于编译期已经了确定了目标方法的参数类型(编译期根据参数的静态类型进行静态分派),因此唯一可以影响到虚拟机选择的因素只有此方法的接收者的实际类型是 Animal 还是 Duck。因为只有一个宗量作为选择依据,所以 Java 语言的动态分派属于单分派类型。
通常运行时确定方法的在执行版本是用invokespecial字节码指令。动态分派的方法版本选择过程需要运行时在类的方法元数据中查找合适的目标方法,但虚拟机基于性能的考虑,大部分的实现都不会真的进行如此频繁的查找,面对这样的情况最常用的“稳定优化“手段是为类在方法区中建立一个虚方法表(virtual method table),使用虚方法表索引来代替元数据查找以提高性能。虚方法表中存着各个方法的实际入口地址,若是某个方法在子类中没有被重写,则此方法在子类虚方法表中的入口地址与父类虚方法表中的入口地址一致,都指向父类的实现入口;如果子类重写了某个方法,则此方法在子类虚方法表中的地址将被替换为子类所实现的入口地址。
参考(部分内容源自):
《深入理解java虚拟机》
https://blog.csdn.net/pange1991/article/details/82080907
https://qinzhaokun.github.io/2017/08/01/Java%E6%96%B9%E6%B3%95%E8%B0%83%E7%94%A8%E8%BF%87%E7%A8%8B%EF%BC%88%E9%9D%99%E6%80%81%E5%88%86%E6%B4%BE%E4%B8%8E%E5%8A%A8%E6%80%81%E5%88%86%E6%B4%BE%EF%BC%89/
https://www.cnblogs.com/hapjin/p/9374269.html