作为一门面向对象语言,Java 拥有封装、继承、多态三大特性。多态就是允许不同类的对象对同一消息做出响应。基于多态,可以消除一些类型耦合关系,实现可替换、可扩充。Java 中使用多态特性的方法主要有,实现一个接口,实现抽象类的一个方法,覆盖父类的一个方法。
多态的底层实现是动态绑定,即在运行时才把方法调用与方法实现关联起来。动态绑定涉及很多 JVM 的细节,全部写清楚需要很大篇幅,因此本文仅简单描述,后续会有其他文章就其中的细节一一分享。
静态绑定与动态绑定
JVM 的方法调用指令有五个,分别是:
invokestatic:调用静态方法;
invokespecial:调用实例构造器<init>方法、私有方法和父类方法;
invokevirtual:调用虚方法;
invokeinterface:调用接口方法,运行时确定具体实现;
invokedynamic:运行时动态解析所引用的方法,然后再执行,用于支持动态类型语言。
其中,invokestatic 和 invokespecial 用于静态绑定,invokevirtual 和 invokeinterface 用于动态绑定。可以看出,动态绑定主要应用于虚方法和接口方法。
静态绑定在编译期就已经确定,这是因为静态方法、构造器方法、私有方法和父类方法可以唯一确定。这些方法的符号引用在类加载的解析阶段就会解析成直接引用。因此这些方法也被称为非虚方法,与之相对的便是虚方法。
虚方法的方法调用与方法实现的关联(也就是分派)有两种,一种是在编译期确定,被称为静态分派,比如方法的重载;一种是在运行时确定,被称为动态分派,比如方法的覆盖。对象方法基本上都是虚方法。
这里需要特别说明的是,final 方法由于不能被覆盖,可以唯一确定,因此 Java 语言规范规定 final 方法属于非虚方法,但仍然使用 invokevirtual 指令调用。静态绑定、动态绑定的概念和虚方法、非虚方法的概念是两个不同的概念。
多态的实现
在前文《Java线程知识拾遗》中提到过,虚拟机栈中会存放当前方法调用的栈帧,在栈帧中,存储着局部变量表、操作栈、动态连接 、返回地址和其他附加信息。多态的实现过程,就是方法调用动态分派的过程,通过栈帧的信息去找到被调用方法的具体实现,然后使用这个具体实现的直接引用完成方法调用。
以 invokevirtual 指令为例,在执行时,大致可以分为以下几步:
- 先从操作栈中找到对象的实际类型 class;
- 找到 class 中与被调用方法签名相同的方法,如果有访问权限就返回这个方法的直接引用,如果没有访问权限就报错 java.lang.IllegalAccessError ;
- 如果第 2 步找不到相符的方法,就去搜索 class 的父类,按照继承关系自下而上依次执行第 2 步的操作;
- 如果第 3 步找不到相符的方法,就报错 java.lang.AbstractMethodError ;
可以看到,如果子类覆盖了父类的方法,则在多态调用中,动态绑定过程会首先确定实际类型是子类,从而先搜索到子类中的方法。这个过程便是方法覆盖的本质。
实际上,商用虚拟机为了保证性能,通常会使用虚方法表和接口方法表,而不是每次都执行一遍上面的步骤。以虚方法表为例,虚方法表在类加载的解析阶段填充完成,其中存储了所有方法的直接引用。也就是说,动态分派在填充虚方法表的时候就已经完成了。
在子类的虚方法表中,如果子类覆盖了父类的某个方法,则这个方法的直接引用指向子类的实现;而子类没有覆盖的那些方法,比如 Object 的方法,直接引用指向父类或 Object 的实现。
每周 3 篇学习笔记或技术总结,面向有一定基础的 Java 程序员,内容涉及 Java 进阶、虚拟机、MySQL、NoSQL、分布式计算、开源框架等多个领域。关注作者或微信公众号 后端开发那点事儿 第一时间获取最新内容。