这部分的笔记在国庆假期前就整理好了,然后说再加工一下,放到 blog 上,毕竟笔记只是适合自己看,没有很强的逻辑性。但每个假期回来后,都会患上一定的假期综合征,总需要几天收收心。就一直拖到现在,简直无可救药。

OOP 的三大特征:封装、继承、多态。以前记忆的时候都是不分先后,随便记忆的,加深理解后才知道这三个特性是一个递进的过程。最后的多态是在继承的基础之上的。

这里先从 JVM 方法调用说起,最后在得出多态的概念。

方法调用指令

在 JVM 中有四个主要的方法调用指令:

  • invokestatic
  • invokespecial
  • invokevirtual
  • invokeinterface

在 Class 文件中,这些方法调用指令后面跟着的是要调用方法的符号引用。这些符号引用最终都要在类加载阶段或运行期转化为直接引用。

其中 invokestatic 和 invokespecial 这两个指令调用的方法称为解析调用,是一个静态过程, 在编译期间就完全确定。要调用方法的符号引用在运行时常量池中已经在类加载的解析阶段被解析为了方法的直接引用,就不需要在运行期去方法表中查找了。这些方法有静态方法、私有方法、实例构造器,称为非虚方法

其中 invokestatic 用于调用静态方法,invokespecial 用于调用私用方法和实例构造器。此外虽然被 final 修饰的方法是使用 invokesvirtual 来调用的,但由于 final 方法无法被子类覆写,只存在唯一版本,所以也是一种非虚方法。这也是这些方法无法被继承或覆写在 JVM 的体现。

而 invokesvirtual 和 invokeinterface 这两个指令调用的方法称为分派调用,是在运行期才能确定正确的目标方法,得到直接引用。而分派调用又分为静态分派和动态分派。

分派调用

分派都是通过方法的符号引用去运行时常量池中去查找得到方法所属类型(即调用该方法的引用变量所属类型,是静态类型)及方法名和描述符,然后根据方法名和描述符去所属类型的方法表中查找确定该方法的目标版本,进而得到方法的直接引用。

静态分派

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。

在 Java 中的体现就是方法重载,在编译阶段,编译器会根据参数的静态类型决定使用哪个重载版本,完全发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。只是在运行期间去静态类型的方法表中查找该方法,从而得到该方法的直接引用。

动态分派

相对于静态分派,动态分派是在运行期根据实际类型来确定方法执行版本的分派过程称为动态分派。

在 Java 中的体现就是方法的覆写。因为实际类型在运行期间可随着程序变化,因此只能在在运行期间才能确定一个对象的实际类型是什么,编译时编译器并不知道,只知道静态类型是什么。那就需要动态的分派目标方法执行版本。这个过程就是重写的本质

但是针对 invokevirtual 和 invokeinterface 两个指令,动态分派的具体过程是不同的:

  • invokevirtual,实例调用,调用对象的实例方法。动态的分派目标方法执行版本是先在静态类型方法表中找到目标方法并得到偏移量,根据栈帧中对象的引用得到这个对象,在从对象中指向方法区类信息和运行时常量池的引用得到对象的实际类型的类信息,然后在得到的类信息方法表相同偏移量的位置查找目标方法
  • invokeinterface, 接口方法调用,在运行时再确定一个实现此接口的对象。因为一个类可以实现多个接口,相当于多继承。所以就不能按照偏移量去实现类的方法表中查找了,只能通过搜索完整的方法表。

不管是动态分派还是静态分派,我们发现都离不开一个关键的东西方法表,最终都是通过方法表进行索引从而得到方法的直接引用。因此方法表是实现动态分派的一个关键。

方法表

JVM 在完成类加载后,会将这个 class 文件二进制字节流转化为虚拟机所需格式存储在方法区中,称为类信息,类信息就是类文件在运行时的数据结构,包含了该类中所有定义的信息。

类信息中包含有一个方法表,方法表中包括从父类(一直到 Object 类)继承的所有实例方法(不包含私有方法,因为私有方法不能继承)以及自身覆写的方法的直接引用,这些直接引用指向类信息中相应的方法代码。

如果是本类的方法或者是覆写了父类的方法,则指向的是本类类信息中相应的方法代码;如果是父类的方法,则指向的是父类类信息中的方法代码。这样通过方法表中方法的引用就可以访问到该类到根类的所有实例方法。

方法表是 JVM 用来提高搜索查找目标方法性能的一个实现。invokevirtual 执行时用到的方法表是虚方法;invokeinterface 用到的方法表是接口方法表。为了程序上实现的方便,具有相同签名的方法,在父类、子类的虚方法表中都具有一样的索引号,因此才可直接使用偏移量在实际类型的方法表中进行查找。

多态

那么什么是多态呢,一般的书籍都是把多态分为两种:

  • 编译时多态,通过方法的重载体现,是静态分派
  • 运行时多态,通过方法的覆写体现,是动态分派

因为重载是静态分派,因此也可以说重载不算是多态的一个体现,但并不妨碍我们的理解。

多态还可以从不同的角度进行定义,如在语法上是子类继承父类并覆写了父类的方法,父类引用指向子类对象。在 OOP 里是 Java 引入了多态性的概念是为了弥补因为单继承而带来的一些不足,如接口也是在一定程度上为了可以使用多继承。

另外重载和覆写说的都是方法,只有类中的方法才有多态的概念,类中的成员变量和内部类并没有此概念。静态方法也没有多态的概念,和成员变量取值与父类还是子类一样,都是由引用变量的静态类型决定的。也因此可以说静态方法并不能被覆写。

参考

内容主要来自 《深入理解 Java 虚拟机》第八章。都是自己理解后总结的,难免有错误之处,以后慢慢修正。