为了不没必要要的浪费时间,文章主要是围绕俩点进行展开:java
一、重载为何根据静态类型,而非动态类型?面试
二、经过动/静态分派决定调用对应方法的符号引用。编程
若是对这俩个问题理解的比较深入的话,这篇文章不看也罢,哈哈~安全
文章后半部分,会从字节码层面,聊一聊符号引用。若是Class文件结构不是很了解的小伙伴,能够选择性观看~或者看看这篇文章,[动态代理三部曲:中] - 从动态代理,看Class文件结构定义多线程
爱因斯坦:“若是你不能简单地解释同样东西,说明你没真正理解它。”ide
[短文速读-2] 重载/重写,动/静态分派?(从新修订)学习
[短文速读-3] 内部匿名类使用外部变量为何要加finalspa
小A:MDove,我最近遇到一个问题百思不得其解。
MDove:正常,毕竟你这智商1+1都不知道为何等于2。
小A:那1+1为啥等于2呢?
MDove:......说你遇到的问题。
小A:是这样的,我在学习多态的时候,重载和重写,有点蒙圈了。我本身写了一个重载和重写的demo...
public class MethodMain {
public static void main(String[] args) {
MethodMain main = new MethodMain();
Language language = new Language();
Language java = new Java();
//重载
main.sayHi(language);
main.sayHi(java);
//重写
language.sayHi();
java.sayHi();
}
private void sayHi(Java java) {
System.out.println("Hi Java");
}
private void sayHi(Language language) {
System.out.println("Im Language");
}
}
public class Java extends Language {
@Override
public void sayHi() {
System.out.println("Hi,Im Java");
}
}
public class Language {
public void sayHi() {
System.out.println("Hi,Im Language");
}
}
复制代码
小A:重写的结果这个毫无疑问。可是为何重载的demo运行结果是这个呀?我以为它应该一个是Im Language一个是Hi Java呀。毕竟我在调用方法时,参数一个传的实例化的类型一个Java,一个是Languae,为啥不一个匹配参数是Java类型,一个匹配参数Language类型啊?
MDove:原来是这个疑惑呀。其实我最初也有这个疑惑。这里借用 R大的一个回答,看回答以前,咱们先明确一个概念:A a = new B()
。这个A a中的A被称之为静态类型,B称为动态类型/实际类型。
MDove:明确这个概念以后,让咱们看一下R大的回答。(这里我抽取了和这个问题相关的内容,若是想了解更多内容,能够去R大的回答里膜拜。)
为什么断定调用哪一个版本的重载只经过传入参数的静态类型来断定,而不使用其动态类型(运行时传入的实际参数的实际类型)来断定?其实根源并不复杂:由于这是当时的常规作法,C++也是这么设计的,因而Java就继承了下来。这样作的好处是设计与实现都简单,并且方法重载在运行时没有任何额外开销...(省略部份内容)...而这么作的缺点也很明显:牺牲了灵活性。若是程序确实须要根据多个参数的实际类型来作动态分派,那就得让码农们本身撸实现了。
小A:没想到是出于这么一种考虑。那咱们又是重载,又是重写。这么多很相似的方法。JVM是怎么选择和调用具体的方法的呢?
MDove:这个过程算是方法的调用,提到这个过程,咱们不得不聊一聊分派这个概念。分派又分两种方式:静态分派、动态分派。这两种方式决定了一个方法的最终执行。
MDove:小A,你觉不以为这两个demo在写法上有明显的不一样么?或者再上升一个高度。重载和重写是否是在业务场景上是有不一样之处的?
小A:你这么一说好像真是!重载是在一个类里边折腾;而重写是子类折腾父类。
MDove:没错,正是如此!咱们总结一下:
MDove:上述你写的那个重载demo里,对于Language language = new Java();
来讲:Language是静态类型,Java是实际类型。一样MethodMain main = new MethodMain();
也是如此,MethodMain main
这个MethodMain是静态类型,new MethodMain()
这是MethodMain是实际类型。
MDove:因此,对于JVM来讲,在编译期参数/及调用者的静态类型是肯定的,所以这一步方法的符号引用是肯定的。(这个过程就是:静态分派)
小A:哦,原来在编译期就已经肯定了符号引用...不对,等等!!若是肯定符号引用也会用过调用者的静态类型,那重写不也是调用静态类型里边的方法了?!!!
MDove:哎呦,你小子反应的还挺快!先回答你的问题,在这个过程当中,重写的确和重载所肯定的符号引用是同样的!咱们看一下你demo中字节码的内容,这里只截取一部分:
红色是重载,紫色是重写
MDove:看到了吧,你说的没错,紫色所标注的内容就是重写的俩个方法在编译期决定的符号引用。由于静态分派的缘由,它们俩的确是相同的!而重载的方法选择就是在这个过程肯定的,可是重写比较特殊,它须要依赖运行时对象的实际类型,所以对于重写来讲它还须要动态分派。
MDove:对于静态分派。说白了就是,在编译期就决定好,调用哪一个版本的方法。所以对于在运行期间生成的实际类型JVM是不关心的。只要你的静态类型是郭德纲,就算你new一个吴亦凡出来。这行代码也不能又长又宽...
MDove:这里总结一下,全部依赖静态类型来定位方法执行版本的方式都称之为:静态分派。
小A:静态分派我明白了,快讲讲动态分派吧!我想知道为何一样的符号引用,重写会执行不一样的方法!
MDove:咱们都知道重写涉及到你是调用子类的方法仍是调用父的。也就是说调用者的实际类型对于重写是有影响的,所以这种状况下仅靠静态分派就行不通了,须要在运行期间的动态分派去进一步肯定。
MDove:此次咱们来看一下这个main方法里边的字节码状况:
MDove:这里简单的解释一下这些字节码指令:
第一个紫色的圈:执行常量池#5的符号应用,并将其压倒操做数栈第2个位置。
第一个紫色的线:就是常量池#5对应的符号引用。咱们能够看出它是一个Language的类型。
第一个红色的圈:执行常量池#6的符号应用,并将其压倒操做数栈第3个位置。
第一个红色的线:就是常量池#6对应的符号引用。咱们能够看出它是一个Java的类型
第二个紫色的圈:取出操做数栈2的变量,执行invokevirtual执行也就是执行#9。
第二个紫色的线:就是常量池#9对应的符号引用。咱们能够看出它Language.sayHi()方法。
第二个红色的圈:取出操做数栈3的变量,执行invokevirtual执行也就是执行#9。
第二个红色的线:就是常量池#9对应的符号引用。咱们能够看出它Language.sayHi()方法。
MDove:经过字节码指令咱们能够看出,除了操做数栈中变量的类型不一样,其余的都是相同的。所以动态分派的特性,就在invokevirtual这个执行中。
MDove:简单来讲,虚拟机在执行invokevirtual时,会先找到操做数栈顶第一个元素,去找它的实际类型,而后找到它对应的符号引用,若是没有就一步步往上找,直到找到。紧接着动态连接到它的真正内存地址,完成方法调用。
MDove:对应到咱们的demo,上就是:执行java.sayHi()时,先是aload_3出栈,肯定它的实际类型,一看是Java类型,因此就会在Java.class中找对应的符号引用,有的话,返回对应的符号引用,执行。也就完成了咱们的重写调用。
小A:Java真好玩,我想回家送外卖...
对于重载来讲,在编译期,就已经经过静态类型决定要选择那个版本的方法了(决定调用哪一个符号引用)。而这种经过静态类型来定位方法执行版本的过程叫作:静态分派。
对于重写来讲,经过静态类型显然是行不通的,所以须要动态类型来判断。那么这种经过动态类型来定位方法执行版本的过程叫作:动态分派。