前言:
眼前大家对“重载方法怎么写”大概比较看重,同学们都需要分析一些“重载方法怎么写”的相关文章。那么小编也在网摘上搜集了一些关于“重载方法怎么写””的相关知识,希望小伙伴们能喜欢,姐妹们快快来学习一下吧!正题
为了避免不必要的浪费时间,文章主要是围绕俩点进行展开:
1、重载为什么根据静态类型,而非动态类型?2、我们重载/重写了这么多方法,是怎么被准确的定位到的?
如果对这俩个问题理解的比较深刻的话,这篇文章不看也罢,哈哈~
文章后半部分,会从字节码层面,聊一聊符号引用和动态链接。如果Class文件结构不是很了解的小伙伴,可以选择性观看~或者看看我历史文章中关于Class文件结构的部分。
引子
小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); } 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 {}public abstract class Language {}
//重写public class MethodMain { public static void main(String[] args) { Language language = new Java(); new MethodMain().sayHi(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:原来是这个疑惑呀。其实我最初也有这个疑惑。由于并没有找到官方的解释,所以自行推测了一下,你仔细看这俩种写法,再思考一下重载和重写写法上的不同,是不是可以总结出来:重载倾向于方法的选择;而重写更在意是谁在调用方法。既然是选择那么就要有明确的标准,而这个标准根据参数的静态类型,这样方便且直接。也就是现在重载的这种实现方式。
PS:A a = new A() 这个A a中的A就是静态类型
MDove:。后来看到R大的一个回答,补充了很多设计上的思路,也侧面证实了我的这种想法:
为何判定调用哪个版本的重载只通过传入参数的静态类型来判定,而不使用其动态类型(运行时传入的实际参数的实际类型)来判定?其实根源并不复杂:因为这是当时的常规做法,C++也是这么设计的,于是Java就继承了下来。这样做的好处是设计与实现都简单,而且方法重载在运行时没有任何额外开销——不同重载版本间在运行时就像是没有任何联系的独立方法一样,因为运行时所关心的“方法名”不只是语言层面的方法名,而也包括了带有参数列表及返回类型信息的方法签名(signature)在内。例如说,JVM并不知道foo方法是什么,而只认foo:()V、foo:(II)Z这样的方法名+方法签名的符号引用。而这么做的缺点也很明显:牺牲了灵活性。如果程序确实需要根据多个参数的实际类型来做动态分派,那就得让码农们自己撸实现了。
方法的定位静态分派
MDove:小A,你难道不觉得,这两个demo在写法上有明显的不同么?或者再上升一个高度。重载和重写是不是在业务场景上是有不同之处的?
小A:你这么一说好像真是!重载是在一个类里边折腾;而重写是子类折腾父类。
MDove:没错,正是如此!我们再深入的思考一下,上文提到的:
重载是更倾向于对方法的选择。而重写则更倾向于是谁在调用方法的调用。
MDove:首先,让我们看一段代码。
A a = new B();
MDove:对于A和B来说,他们有不同的学术名词。A称之为静态类型,B称之为实际类型。对于Language language = new MethodMain().new Java();也是如此:Language是静态类型,Java是实际类型。
MDove:从你写的demo里,我们可以看出来:main.sayHi(language); main.sayHi(java);最终都是调用了private void sayHi(Language language)。我们是可以得出一个结论:方法的调用,在选择重载哪个版本的时候,是根据静态类型去匹配的。
就像你的那个demo一样,language和java的静态类型都是Language所以就匹配了private void sayHi(Language language)这个方法。
MDove:在调用之前,我们再回到上文提到的静态类型上。对于JVM来说,在编译期变量的静态类型是确定的,同样重载的方法也就能够确定,同样也间接的提高了性能。这很好理解,因为二者都是确定无误的。所以对于这种方法的调用,就叫静态分派。(因为这类不涉及任何需要动态决定类型的地方)
MDove:Java最初的设计就是如此,对于方法的选择(比如说重载)来说,在编译期根据静态类型决定。这叫做:静态分派。而重写因为其特殊性,它只有在运行时,才能确定实例对象是什么,因此它的这种方法调用就被称之为:动态分派。
小A:原来如此,那可不可以再多讲一讲动态分派呢?
MDove:如果要聊动态分派,那就必须要引出一些概念。先用一句话总结这个过程:
我们的java文件被编译成class文件后,class文件中的常量池部分就拥有了和这个类相关的符号引用。在类加载时通过静态分派决定某些方法的调用,在运行期间通过动态分派决定某些方法的调用。当定位到对应方法的符号引用后,字节码指令会执行对应的符号引用,然后将这些符号引用真正映射到对应对象的内存地址上。这样就可以完成真正方法的调用。
动态分派
MDove:刚才提到的静态分派的过程。
MDove:说白了就是,在编译期就决定好该怎么调用这个方法。因此对于在运行期间生成的实际类型JVM是不关心的。只要你的静态类型是郭德纲,就算你new一个吴亦凡出来。这行代码也不能又长又宽…
小A:照这个逻辑来说,重写就是动态分派,需要JVM在运行期间确定对象的实际类型,然后再决定调用哪个方法。
MDove:没错,毕竟重写涉及到你是调用子类的方法还是调用父类。也就是说调用者的类型对于重写是有影响的,因此这种情况下静态分派就行不通了,需要在运行期间去决定。
MDove:当然我们用嘴说是很轻巧的,实际JVM去执行时是很复杂的过程。根据你上边写的重写的代码,咱们来从字节码层面看一看方法的调用过程。这个是你的那个demo,javap的内容:
MDove:我用三种颜色标注的地方,就是我们需要关注的点。我们的方法调用,也就是通过常量池里#X的符号引用进行关联的(这些内容都是在编译期生成的)。那么对于我们重写来说,我们需要运行期决定类型,也就是说在运行期间决定到底调用哪个方法。
MDove:注意一下黄色圈起来的字节码指令:invokespecial。invokespecial以及invokestatic指令所执行的符号引用,在类被加载的时候就直接替换成了直接引用,并不会等到运行期。因此它的执行简单明了(就比如:执行我们的重载,静态分派决定方法,找到对应符号引用,拿到直接引用执行即可)。
MDove:而其他的指令,例如紫色圈起来的invokevirtual则不同。
MDove:简单来说,虚拟机在执行invokevirtual时,会先找到操作数栈顶第一个元素,去找它的实际类型,然后找到它对应的符号引用,如果没有就一步步往上找,直到找到(这也就是咱们重写的原理)。紧接着动态链接到它的真正内存地址,也就是我们子类重写的方法上。完成方法调用。
小A:那它的内存地址是怎么动态链接上的?
MDove:那这个问题就比较的复杂了,如果很感兴趣的话,可以看一下R大的回答。
小A:Java真好玩,我想回家送外卖…
总结
对于重载来说,在编译期,就已经通过静态类型决定要选择那个版本的方法了(决定调用哪个符号引用)。而这种通过静态类型来定位方法执行版本的过程叫做:静态分派。
对于重写来说,通过静态类型显然是行不通的,因此需要动态类型来判断。那么这种通过动态类型来定位方法执行版本的过程叫做:动态分派。
剧终
标签: #重载方法怎么写