写在最前
对于从事java开发的人一定见过这些概念,但是不去仔细研究却又不清楚这些都是什么,有时候又感觉几个感念在说一个事情,云里雾里,我也是在这种情况下对这几种概念进行学习的,这里对他们的联系进行总结。
一.这些说的到底是什么
1.连接
在类的加载过程中有这些步骤:加载–>验证–>准备–>解析–>初始化这五个步骤,其中解析这个步骤的目的就是将符号引用(指向的是未被加载进内存的数据)转化为直接引用(指向虚拟机内存中的地址),像这种在类解析阶段就将符号引用转化为直接引用的过程被称为静态连接,方法在真正运行之前就会有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可变的,也可以这么说“编译器可知,运行期不可变”这类方法的调用被称为解析。
哪些方法的调用会在解析阶段将符号引用转为直接引用?
答案就是非虚方法,那什么是非虚方法呢?构造器、私有方法、父类方法、静态方法、final修饰的方法被称为非虚方法,他们使用invokestatic、invokespecial指令控制,非虚方法的调用会在类加载的解析阶段将其符号引用转化为直接引用。
静态连接与非虚方法的关系
非虚方法的调用会在类的解析阶段将符号引用转化为直接引用,这个过程叫静态连接。
1.2动态连接 与 虚方法
与静态连接相对应,有部分的方法调用在类加载的解析阶段是不能够转为直接引用的,因为他们调用的具体方法在这个阶段是不能确定的,比如常见的面向父类、面向接口编程等。这部分的方法调用只有在运行期才能够将符号引用转化为直接引用,这部分就是动态连接。
1.哪些方法的是属于动态连接的范畴呢?
上面已经说过非虚方法的定义,那么除了非虚方法,其他方法都是虚方法了,比如接口方法的调用、实例化方法的调用这些都是虚方法调用,在指令层面就是invokevirtual、invokeinterface、invokedynamic这些指令都应用于虚方法的调用,这其中有一个特例就是final修饰的方法是非虚方法,单仍使用invokevirtual指令。
2.动态连接与虚方法的关系
非虚方法会在程序的运行阶段将符号引用转化为直接引用,这个过程叫动态连接。
2.分派
静态连接与动态连接描述的是符号引用转化为直接引用的这个过程或者说这个动作。分派和连接并不是一个层次的概念,而分派描述的是方法版本确定的过程。
2.1静态分派
分派可分为静态分派动态分派,java中的重载与重写则是静态分派与动态分派的最好例子,在具体介绍静态分派之前,我们先看下java中方法重载的例子。
public class TestDispath { static abstract class Father{ } static class Son extends Father{ } static class Daughter extends Father{ } public void test(Father father){ System.out.println("我是父亲"); } public void test(Son son){ System.out.println("我是儿子"); } public void test(Daughter daughter){ System.out.println("我是女儿"); } public static void main(String[] args) { Father son = new Son(); Father daugther = new Daughter(); TestDispath testDispath = new TestDispath(); testDispath.test(son); testDispath.test(daugther); } }
上面是一个方法重载的例子,输出结果如下所示:
我是父亲 我是父亲 Process finished with exit code 0
那为什么真正调用的都是test(Father father)这个方法呢,先来了解下两个概念。
静态类型:上方代码中 Father son = new Son(); 其中Father就是变量son的静态类型。
实际类型:上方代码中 Father son = new Son(); 其中Son就是变量son的实际类型。
根据上方的输出显示,我们可以看出重载时方法调用是根据变量的静态类型来确定的,那我们就可以得出静态分派的定义了,“所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派”,java中的方法重载就是静态分派,且静态分派是在编译器就已经完成的了。运行期不会改变,所以也有把静态分派归为解析范畴的。
2.2动态分派
我们已经知道“根据变量的静态类型来决定方法的调用的分派动作叫静态分派”,那与之对应的“根据实际类型来决定方法的分派动作动态分派”,动态分派的例子就是我们常用的重写,下面我们来看一个重写的的例子。
public class TestDynamicDispath { static abstract class Father{ public abstract void test(); } static class Son extends Father{ @Override public void test() { System.out.println("测试儿子"); } } static class Daughter extends Father{ @Override public void test() { System.out.println("测试女儿"); } } public static void main(String[] args) { Father son = new Son(); Father daughter = new Daughter(); son.test(); daughter.test(); son = new Daughter(); son.test(); } }
上方代码的输出结果如下:
测试儿子 测试女儿 测试女儿 Process finished with exit code 0
从上方输出结果可以看出,真正调用的哪个方法完全取决于变量的实际类型。
2.3 单分派、多分派
分派中根据“宗量”,又可以把分派分为单分派和多分派。那什么是宗量呢?
方法的接收者与方法的参数统称为宗量,根据宗量的多少可以将分派分为单分派和多分派。根据一个宗量对方法进行选择叫单分派,根据多于一个宗量对方法进行选择就叫多分派。
1.java中的静态分派是单分派还是多分派?
在重载中,调用方法,影响方法调用的因素有两个,一个是方法接收者(即方法调用者),和传入的参数,方法的调用者不同或者方法的传参不同都会调用到不同的方法。所以java中的静态分派属于静态多分派。
2.java中的动态分派是单分派还是多分派?
在重写中,调用方法,影响方法调用的因素只有一个,就是方法的接收者,也就是方法的实际类型,所以java的动态分派属于动态单分派。
所以java的分派属于静态多分派与动态单分派。
二.全文总结
静态连接与动态连接描述的是符号引用转化为直接引用的动作。虚方法与非虚方法则是根据方法在何时转化为直接引用来对方法种类的一种划分,分派描述的则是方法调用时方法的选择过程。他们之间都有联系,但说的都是不同的事情。我们可以用这么一段话对他们进行总结:非虚方法会在类加载的解析阶段将符号引用转化为直接引用这个过程叫静态连接。虚方法会在程序运行阶段将符号引用转化为直接引用这个过程叫动态连接,在动态连接的过程中方法具体的调用则是分派的过程,分派根据由静态类型决定还是实际类型决定又分为静态分派和动态分派,分别对应着java中的重载与重写,由此我们还得出了,java是一门静态多分派与动态单分派的语言(静态分派其实发生在编译阶段,因此也有人将静态分派划为解析,如果将静态分派算在分派中,后半句描述就会有些问题)。