方法的调用不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。
一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局的入口地址(入口地址相当于直接引用),需要在类加载期间甚至到运行期间才能确定目标方法的直接引用。
通过javap -verbose+类名 可以看到,Java虚拟机中提供了四条方法调用字节码指令,
1.invokestatic 调用静态方法
2.invokespecial 调用实例构造器(init)方法、私有方法和父类方法
3.invokevirtual 调用所有的虚方法
4.invokeinterface 调用接口方法,会在运行时再确定一个实现此接口的对象
先来看下面两种方法调用方式:
解析调用:方法在程序真正运行前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。(一定是静态的)
分派调用:方法在程序运行期间才确定调用版本,分派调用分为静态分派和动态分派
再来理解一个概念:
非虚方法:在类加载的时候就会把符号引用解析为该方法的直接引用,就是说能在解析阶段确定唯一的方法调用版本的方法称为非虚方法。只要能被invokestatic和invokespecial指令调用的方法,都符合非虚条件,包括了静态方法、私有方法、实例构造器和父类方法四类,还有一个特殊的final方法,它虽然通过invokevirtual调用,但Java语言规范中明确说明了final方法是一种非虚方法。
虚方法:与之相反的,在运行期才能确定方法调用版本。
所以非虚方法都是通过解析调用(静态),而虚方法都是通过分派调用(静态或者动态)。
那么在分派和解析调用中的静态和动态的概念又怎么理解呢,请看下面这个例子:
Human person = new Man();
Man是Human的一个子类,那么Human就是实例person的静态类型,Man是person的实际类型。
1.下面这段代码演示了方法重载(overload)时是通过静态分派的:
public class StaticDispatch {
static abstract class Human{
}
static class Man extends Human{
}
static class Woman extends Human{
}
public void sayHello(Human m)
{
System.out.println("hello, guy!");
}
public void sayHello(Man m)
{
System.out.println("hello, man!");
}
public void sayHello(Woman m)
{
System.out.println("hello, woman!");
}
public static void main(String[] args) throws Exception {
Human man = new Man();
Human woman = new Woman();
StaticDispatch t = new StaticDispatch();
t.sayHello(man);
t.sayHello(woman);
}
}
运行结果是:
hello, guy!
hello, guy!
2.下面这段代码演示了方法重写(overwrite)时是动态分派的:
public class DynamicDispatch {
static abstract class Human{
protected abstract void sayHello();
}
static class Man extends Human{
@Override
protected void sayHello() {
System.out.println("hello, man!");
}
}
static class Woman extends Human{
@Override
protected void sayHello() {
System.out.println("hello, woman!");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
}
}
运行结果是:
hello, man!
hello, woman!
接下来看以下invokevirtual指令调用方法的过程就可以理解为什么如此了:
(1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C
(2)如果在类型C中找到与常量中描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找结束;不通过,返回IllegalAccessError
(3)否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程
(4)始终没有找到合适的方法,则抛出AbstractMethodError
其实方法的重载和重写区别就是重载依赖传入方法的形参来区别,而重写依赖方法调用者的类型来区别。
在第一步中C就是方法的调用者,这里查找的是实际类型,这就是overwrite的原理,
在第二步中查找方法是通过描述符和简单名称来进行的,而传入参数的描述符是通过静态类型定义的,所以代码一会出现这样的结果
总结一句话,重载看静态类型,重写看实际类型。