假设要调用x.f(args),x是类C的一个对象,那么调用过程如下:
-
编译器查看对象的声明类型和方法名。假设调用x.f(args),且隐式参数x声明为C类的对象。如果存在重载方法,例如f(int)和f(String),那么编译器会一一列举所有C类中名为f的方法和其超类中访问属性为public的方法(private属性无法访问)。
至此,编译器已获得所有可能被调用的候选方法。 -
接下来,编译器将查看调用方法时提供的参数列表,若存在一个与提供的参数列表完全匹配的方法,则调用此方法,这个过程称为重载解析。
如:
对于调用x.f(“Hello World”),那么编译器将调用f(String)而非f(int)。但是由于存在类型转换,如int可以转为double,子类可以转换为超类,因此这个过程稍微有点复杂。
至此,编译器已获得需要调用的方法名字和参数类型。 -
如果是private方法、static方法、final方法或者是构造函数,那么编译器将会准确地知道该调用哪个方法,称这种调用方式为静态绑定。与此相对应的是动态绑定,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。例如,编译器采用动态绑定的方式生成一条调用f(String)的命令。
-
当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最符合的那个类方法。假设x的实际类型是D,它是C的子类。如果D类定义了方法f(String),就直接调用它,否则将在D类的超类中寻找f(String),依次类推。
虚拟机预先为每个类创建了一个方法表,其中列出了所有方法的签名和实际调用的方法,每次在调用方法的时候,虚拟机都会来查找这个表。
例如,有两个类,一个是Employee,一个是Manager,其中Employee是Manager的超类。
类的粗略实现如下:
class Employee
{
...
public double getSalary();
...
}
class Manager extends Employee
{
...
public double getSalary(); //重载超类的方法
}
注意,在Manager中重载了超类Employee的getSalary()方法。
假如此时声明一个Employee类
Employee e=new Employee();
并且调用方法e.getSalary().
由于这个方法没有参数,因此不会有重载解析。
由于getSalary()不是private方法、static方法、final方法,所以将采用动态绑定。虚拟机为Employee和Manager分别生成方法表。在运行时,调用e.getSalary()的过程为:
1、首先,虚拟机提取e的实际类型的方法表,既可能是Employee、Manager的方法表,也可能是Employee的其它子类的方法表
2、接下来,虚拟机搜索定义getSalary签名的类。此时,虚拟机已经知道应该调用哪个方法
3、最后,虚拟机调用方法
动态绑定有一个重要特性,即无需对现有代码做任何修改,就可以对程序进行扩展。
假设新增一个新类Executive,并且变量e有可能引用这个类的对象,那么无需对e.getSalary()进行重新编译,就可以调用Executive.getSalary()方法