简单介绍Java的静态分派和动态分派
2019-02-20 00:45:39来源:博客园 阅读 ()
最近复习JVM的知识,对于静态分派和动态分派的理解有点混乱,于是自己尝试写写代码,在分析中巩固知识。
有如下一段代码,请问每一段分别输出什么?
1 package com.khlin.my.test; 2 3 class Base { 4 5 public static void foo() { 6 System.out.println("Base.foo() invoked"); 7 } 8 9 public void bar(int c) { 10 System.out.println("Base.bar(int) invoked"); 11 } 12 13 public void bar(Character c) { 14 System.out.println("Base.bar(Character) invoked"); 15 } 16 17 public void baz(Object o) { 18 System.out.println("Base.baz(Object) invoked"); 19 } 20 21 public void baz(Integer i) { 22 System.out.println("Base.baz(Integer) invoked"); 23 } 24 25 } 26 27 class Child extends Base { 28 public static void foo() { 29 System.out.println("Child.foo() invoked"); 30 } 31 32 public void bar(Character c) { 33 System.out.println("Child.bar(Character) invoked"); 34 } 35 36 public void bar(char c) { 37 System.out.println("Child.bar(char) invoked"); 38 } 39 } 40 41 public class App { 42 43 public static void main(String[] args) { 44 Base child = new Child(); 45 46 System.out.println("第1段输出:"); 47 child.foo(); 48 child.bar(new Character('C')); 49 50 System.out.println("第2段输出:"); 51 Object integer = new Integer(100); 52 child.baz(integer); 53 54 System.out.println("第3段输出:"); 55 child.bar('C'); 56 57 } 58 }
下面我简单地介绍一下从代码编译到方法调用的整个过程。
· 编译
先看看第1段输出,child.foo()是调用父类还是子类的静态方法呢?
在编译阶段,发生了静态分派。
1 Base child = new Child();
在我们创建一个对象时,如上图,Base称为变量的的静态类型(Static Type), 或者叫做外观类型(Apparent Type),后面的Child则称为变量的实际类型(Actual Type)。
所有依赖静态类型来定位方法执行版本的分派动作,称为静态分派。静态分派的典型应用是方法重载,其发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。
方法的接收者(Reciever) 和方法的参数统称为方法的宗量,根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。
在静态分派的时候,选择目标方法的依据有两点,一是静态类型是Base还是Child,二是方法的参数类型。因此,静态分派是多分派。
接下来,我们来看看“第1段输出”代码生成的指令。通过javap -v App.class指令得出如下结果,可以看到第18和第31行两条指令的符号引用,和上述分析一致:child的静态类型是Base,所以选择Base类的方法;通过无参数和Character类型,分别确定是具体哪个方法版本。
但最终两者的行为不一样,child.foo() 调用的是静态类型Base的foo(),而child.bar(new Character('C')) 则是调用实际类型Child的方法。
原因就是出在两条指令不一样:invokestatic和invokevirtual
在Java虚拟机里面提供了5条方法调用字节码指令:
invokestatic:调用静态方法
invokespecial:调用实例构造器<init>方法、私有方法和父类方法
invokevirtual: 调用所有的虚方法
invokeinterface:调用接口方法,会在运行时再确定 一个实现此接口的对象
invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic是由用户所设定的引导方法决定的。
具体原因是不同的指令在下一阶段(类加载的解析)的行为不一样,暂时先放到一边,我们再看看第2段输出的指令。
可以看出,在静态分派时,是根据传入方法的参数的静态类型来决定调用的方法版本,虽然有baz(Integer)的方法,但是传入的参数integer的静态类型是Object,所以调用了baz(Object)。
再来看看第3段输出的指令,我们知道符号引用肯定还是Base类里的方法(尽管Child类里有参数一样的bar(char c) 方法),但Base里没有一模一样参数(char类型) 的方法,不会报错吗?会调用哪个方法呢?
原来,编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是“唯一的”,往往只能确定一个“更加合适”的版本。
具体可参考:https://www.cnblogs.com/kingsleylam/p/6789119.html
· 类加载之解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法,私有方法,实例构造器,父类方法4类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以称为非虚方法,其他方法称为非虚方法(除了final方法)。
final修饰的方法,虽然是使用invokevirtual指令来调用,但由于它无法被覆盖,没有其他版本,因此也是非虚方法。具体参考:https://www.cnblogs.com/kingsleylam/p/6765426.html
回到第1段输出,child.foo()是invokestatic指令,那么在解析阶段,就会替换成直接引用,具体的类也就确定下来了,因此调用的是静态类型Base.foo()。
而child.bar(new Character('C')) 是invokevirtual, 在这个阶段可以确定调用的方法签名,但还不能确定方法的接收者的实际类型。它将由动态分派来完成确定。由于只有一个宗量影响,因此动态分派是单分派。
方法接收者的实际类型在下一阶段确定。
· 运行期的方法调用
在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
具体可以参考:https://www.cnblogs.com/kingsleylam/p/6789989.html
最终输出结果是:
参考资料:《深入理解Java虚拟机》第2版 周志明著
原文链接:https://www.cnblogs.com/kingsleylam/p/10390323.html
如有疑问请与原作者联系
标签:
版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有
- 国外程序员整理的Java资源大全(全部是干货) 2020-06-12
- 2020年深圳中国平安各部门Java中级面试真题合集(附答案) 2020-06-11
- 2020年java就业前景 2020-06-11
- 04.Java基础语法 2020-06-11
- Java--反射(框架设计的灵魂)案例 2020-06-11
IDC资讯: 主机资讯 注册资讯 托管资讯 vps资讯 网站建设
网站运营: 建站经验 策划盈利 搜索优化 网站推广 免费资源
网络编程: Asp.Net编程 Asp编程 Php编程 Xml编程 Access Mssql Mysql 其它
服务器技术: Web服务器 Ftp服务器 Mail服务器 Dns服务器 安全防护
软件技巧: 其它软件 Word Excel Powerpoint Ghost Vista QQ空间 QQ FlashGet 迅雷
网页制作: FrontPages Dreamweaver Javascript css photoshop fireworks Flash