- 重载overload:同一个类的多个方法,方法名相同,方法入参不同。——静态分派
- 重写overrite:父和子之间,子类重写父类的方法。——动态分派
java虚拟机中提供了5条方法调用字节码:
- invokestatic:调用静态方法
- invokespecial:调用实例构造器方法、私有方法、父类方法
- invokevirtual:调用所有的虚方法
- invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象
- invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。上面四条调用指令,分派逻辑固化在虚拟机内部,而invokedynamic的分派逻辑由用户设定的引导方法决定。
只要能被invokestatic和invokespecial调用的方法,都可以在解析阶段唯一确定一个调用版本,符合这个条件的有静态方法、构造方法、私有方法、父类方法4类,它们在类加载的解析阶段把方法的符号引用解析成直接应用。这些方法被称为非虚方法。其他方法都称为虚方法。(final方法除外,final方法的字节码指令是invokevirtual,但是也在类加载时解析成直接引用,因为不能被重写)。
解析调用是静态的过程,在编译器就完全确定。而分派调用则可能是静态的也可能是动态的,根据分派的宗量分为单分派和多分派。
静态分派
依赖静态类型来定位方法执行版本的分派动作叫静态分派。
Human man = new Man();
这行代码中,Human叫做变量的静态类型,Man叫做变量的实际类型。静态类型和实际类型都可以发生变化,实际类型的变化在运行期间得知,静态类型的变化在编译时可知。
在编译阶段,java编译器是根据变量的静态类型决定使用哪个重载版本。上面的例子中,man和woman的静态类型都是Human,所以编译器都使用的是sayHello(Human )方法。
动态分派
在运行期,根据实际类型确定方法执行版本的分派过程叫动态分派。
动态分派的执行过程是:
(1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
(2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果成功则返回该方法的直接饮用,查找过程结束;如果不通过,返回java.lang.IllegalAccessError。
(3)否则,按照继承关系从下往上依次对C的各个父类进行(2)的搜索和验证过程。
(4)如果始终没有找到,则抛出java.lang.AbstractMethodError异常。
上面的例子中,man一开始的实际类型是Man,使用动态分派,找到的方法版本是Man的sayHello。
单分派和多分派
方法的接收者和方法的参数统称为方法的宗量。
上面的例子中,father的静态类型是Father,实际类型是Father;son的静态类型是Father,实际类型是Son。在编译阶段静态分派的过程,选择方法版本时,需要考虑方法的接收者和方法的入参,属于静态多分派。分派结果是产生了两条invokevirtual字节码指令,指向的方法版本分别是常量池中的Father.hardChoice(360)和Father.hardChoice(QQ)。 然后是运行期java虚拟机的选择,也就是动态分派的过程,此时在执行invokevirtual的Father.hardChoice(QQ)指令时,已经确定了方法入参的签名必须是QQ,此时唯一可以影响选择的只有方法的接收者,所以是动态单分派。此时son的实际类型是Son,所以选择的结果是Son.hardChoice(QQ)方法。