在 Java 面向对象的三大特征封装、继承、多态中,多态对于刚接触的人来说往往较难理解。理解它的原理有助于我们更深一步的认识。
我们知道,Java 中的多态表现为同一个行为具有多个不同表现形式,使得我们可以通过父类的引用指向子类的方法,比如下面这样:
class Father {
public void g() {
System.out.println("father's g()");
}
}
class Son extends Father {
public void g() {
System.out.println("son's g()");
}
}
public class Polymorphic {
public static void main(String[] args) {
Father f = new Son();
f.g();
}
}
输出结果:
son's g()
那程序是怎么知道调用的是子类的方法呢?
绑定
绑定指的是一个方法的调用与方法所在的类(方法主体)关联起来。对 Java 来说,绑定分为静态绑定和动态绑定,或者叫做前期绑定和后期绑定。
- 静态绑定:在程序执行前方法已经被绑定(也就是说在编译过程中就已经知道这个方法到底是哪个类中的方法),由编译器实现。C就是典型的前期绑定。Java 中 final,static,private 修饰的方法和构造方法是前期绑定
- 动态绑定:运行时根据具体对象的类型进行绑定。
在 Java 编程思想中,作者将多态称作动态绑定;在其他一些地方,将方法重载(overload)称为编译时多态,方法覆盖(override)称为运行时多态(编译时与运行时)。大家只要理解了原理即可,不必过分纠结于概念。
加载过程
Java 代码首先会通过 javac 编译成字节码文件,此时代码并没有放进内存。当需要类加载的时候,JVM 会执行加载类的全过程,包括
- 加载
- 验证
- 准备
- 解析
- 初始化
动态绑定发生在加载类的解析阶段。
现在假设当前代码所处的类为D,要把一个从未解析过的符号引用N解析为一个类或接口C(可以理解为代码里的Son对象)的直接引用(参考深入理解Java虚拟机第二版),步骤如下:
- 如果C不是数组类型,虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。如果加载过程出现任何异常,则解析失败。
- 如果C是数组类型,并且数组元素类型为对象,即N的描述符是类似 “[Ljava/lang/Integer”的形式,按照第一点的规则加载数组元素类型,接着由虚拟机生成一个代表此数组维度和元素的数组对象。
- 如果上面步骤没有出现异常,则C在虚拟机中已经成为一个有效的类或接口,之后会进行符号引用验证,确认D是否具备对C的访问权限,否则抛出异常。
常见概念解释:
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
- 全限定名:如果一个类叫 Hello,所在的包为 com.lslxy,则其全限定名为 com/lslxy/hello
- 简单名称:没有类型和参数修饰的方法或者字段名称
- 描述符:字段的数据类型、方法的参数列表(包括数量,类型及顺序)和返回值
- 方法签名:Java 代码中的方法签名包括方法名称、参数顺序和参数类型,字节码中的方法签名包括方法名称、参数顺序、参数类型、返回值、受查异常表