简单介绍Java的静态分派和动态分派
最近复习JVM的知识,对于静态分派和动态分派的理解有点混乱,于是自己尝试写写代码,在分析中巩固知识。
有如下一段代码,请问每一段分别输出什么?
package com.khlin.my.test;
class Base {
public static void foo() {
System.out.println("Base.foo() invoked");
}
public void bar(int c) {
System.out.println("Base.bar(int) invoked");
}
public void bar(Character c) {
System.out.println("Base.bar(Character) invoked");
}
public void baz(Object o) {
System.out.println("Base.baz(Object) invoked");
}
public void baz(Integer i) {
System.out.println("Base.baz(Integer) invoked");
}
}
class Child extends Base {
public static void foo() {
System.out.println("Child.foo() invoked");
}
public void bar(Character c) {
System.out.println("Child.bar(Character) invoked");
}
public void bar(char c) {
System.out.println("Child.bar(char) invoked");
}
}
public class App {
public static void main(String[] args) {
Base child = new Child();
System.out.println("第1段输出:");
child.foo();
child.bar(new Character('C'));
System.out.println("第2段输出:");
Object integer = new Integer(100);
child.baz(integer);
System.out.println("第3段输出:");
child.bar('C');
}
}
下面我简单地介绍一下从代码编译到方法调用的整个过程。
· 编译
先看看第1段输出,child.foo()是调用父类还是子类的静态方法呢?
在编译阶段,发生了静态分派。
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版 周志明著
简单介绍Java的静态分派和动态分派的更多相关文章
- JAVA的静态代理与动态代理比较--转载
扩展:http://www.ibm.com/developerworks/cn/java/j-lo-proxy1/JAVA的静态代理与动态代理比较 一.概念 代理模式是常用的Java 设计模式,它的特 ...
- Java基础-静态代理与动态代理比较
JAVA的静态代理与动态代理比较 静态代理类: 由程序员创建或由特定工具自动生成源代码,再对其编译.在程序运行前,代理类的.class文件就已经存在了.动态代理类: 在程序运行时,运用反射机制动态创建 ...
- (转)简单介绍java Enumeration
简单介绍java Enumeration 分类: java技术备份 java数据结构objectstringclass存储 Enumeration接口 Enumeration接口本身不是一个数据结构 ...
- Java静态分派与动态分派(二)
方法调用并不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程. 在程序运行时,进行方法调用是最普遍.最频繁的操作,但是Class文件 ...
- java中的静态分派和动态分派
多态是java的基本特征之一,多态即一个对象具有多种形态(多种表达形式,猴子是动物的一种的表现形式),例如:子类是父类的一种形态. 当方法重载时,就会涉及到多态. 1:在重载时是通过参数的静态类型,而 ...
- 转:【深入Java虚拟机】之五:多态性实现机制——静态分派与动态分派
转载请注明出处:http://blog.csdn.net/ns_code/article/details/17965867 方法解析 Class文件的编译过程中不包含传统编译中的连接步骤,一切方法 ...
- 深入Java虚拟机:多态性实现机制——静态分派与动态分派
方法解析 Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址.这个特性给Java带来了更强大的动态扩 ...
- 【深入Java虚拟机】之五:多态性实现机制——静态分派与动态分派
方法解析 Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址.这个特性给Java带来了更强大的动态扩 ...
- Java静态分派和动态分派
前言 动态分派和静态分派机制是Java多态实现的原理.本文将针对这两种机制进行浅析. 静态分派 静态分派机制最典型的代码示例如下 void test() { Father father = new S ...
随机推荐
- 用Jmeter制造软件测试数据
在测试过程难免会遇到一些需要大量测试数据的情况,如果数据没有太多的需求,或者需求本身比较简单,这时候我们可以用Jmeter参数化来实现(建议用badboy录制) 首先,我们可以通过badboy来录制实 ...
- 百度地图SDK的使用
最近看了一些SDK相关的东西,就心血来潮用了一下百度地图的sdk. 百度的文档真的很有问题,配置步骤也错漏很多. 1.首先百度地图的demo一直都是和最新的android studio版本不搭的,问题 ...
- C# 生成序号不足补0
int Num=13 var code =Num.ToString().PadLeft(4, '0'); code:0013
- leetcode707
数据结构的题,从网上找到的实现方式,先记录下来. class MyLinkedList { public: /** Initialize your data structure here. */ My ...
- 无法访问win8默认共享(如C$)解决办法
可以使用此过程允许作为本地 Administrators 组的成员并使用密码身份验证登录的用户在会话过程中使用其管理权限.启动注册表编辑器.单击“开始”,在“开始搜索”框中键入 regedit,然后按 ...
- U3D 代码自动化生成定制预置体的旋转问题
//定制预置体 //要求:1,模型面向U3D的Z轴正向(由MAX导出时是面向U3D的X负向的) //2,增加一些常用挂点,3增加一个圆形阴影片,4,添加包围盒 //根据这些要求制作预置休 static ...
- bootstrap 的页码显示问题-------------德州
之前一个小bug,无论上边怎么搜索,下边的页码,不会改变 调整: 1,在mapper中添加计数, 2,找到service,queryPage中添加, 3,关键一部,如果没有会报错:,找不到该列 so, ...
- Centos 安装旧版php5.2
# yum remove php-* # cd /root/ && mkdir new_php && cd new_php # wget -r http://yum.m ...
- linux shell 脚本攻略学习 -- head命令详解, tail命令详解
当要查看上千行的大文件时,我们可不会用cat命令把整个文件内容给打印出来,相反,我们可能只需要看文件的一小部分地内容(例如文件的前十行和后十行),我们也有可能需要打印出来前n行或后n行,也有可能打印除 ...
- go_指针
值传递:相当于在内存中拷贝一分变量出来,拷贝变量的改变对原变量不影响 引用传递:直接引用内存中的变量,会被改变 c,c++值传递和引用传递都有 Java,python多用引用传递 go语言指针简单之处 ...