弄清楚如何在对象上应用方法调用非常重要。下面假设要调用 x.f(args,) 隐式参数 x 声明为类 C 的一个对象。下面是调用过程的详细描述:

1 ) 编译器査看对象的声明类型和方法名。假设调用 x.f(param,) 且隐式参数 x 声明为 C类的对象。需要注意的是: 有可能存在多个名字为 f, 但参数类型不一样的方法。例如,可能存在方法 f(im) 和方法 String。) 编译器将会一一列举所有 C 类中名为 f 的方法和其超类中访问属性为 public 且名为 f 的方法(超类的私有方法不可访问)。

至此, 编译器已获得所有可能被调用的候选方法。

2 ) 接下来,编译器将査看调用方法时提供的参数类型。如果在所有名为 f 的方法中存在一个与提供的参数类型完全匹配, 就选择这个方法。这个过程被称为重载解析( overloadingresolution)。 例如,对于调用 x.f(“ Hello” )来说, 编译器将会挑选 f(String,) 而不是 f(int。)由于允许类型转换( int 可以转换成 double, Manager 可以转换成 Employee, 等等,) 所以这个过程可能很复杂。 如果编译器没有找到与参数类型匹配的方法, 或者发现经过类型转换后有多个方法与之匹配, 就会报告一个错误。

至此, 编译器已获得需要调用的方法名字和参数类型。

注释: 前面曾经说过,方法的名字和参数列表称为方法的签名。例如, f(int) 和 f(String)是两个具有相同名字, 不同签名的方法。如果在子类中定义了一个与超类签名相同的方法, 那么子类中的这个方法就覆盖了超类中的这个相同签名的方法。

不过, 返回类型不是签名的一部分, 因此, 在覆盖方法时, 一定要保证返回类型的兼容性。 允许子类将覆盖方法的返回类型定义为原返回类型的子类型。 例如, 假设Employee 类有

public Employee getBuddyO { . . . }

经理不会想找这种地位低下的员工。 为了反映这一点, 在后面的子类 Manager 中,可以按照如下所示的方式覆盖这个方法

public Manager getBuddyO { . . . } // OK to change return type

我们说, 这两个 getBuddy 方法具有可协变的返回类型。

3 ) 如果是 private 方法、 static 方法、 final 方法(有关 final 修饰符的含义将在下一节讲述)或者构造器, 那么编译器将可以准确地知道应该调用哪个方法, 我们将这种调用方式称为静态绑定(static binding ) 与此对应的是,调用的方法依赖于隐式参数的实际类型, 并且在运行时实现动态绑定。在我们列举的示例中, 编译器采用动态绑定的方式生成一条调用 f(String) 的指令。

4 ) 当程序运行,并且采用动态绑定调用方法时, 虚拟机一定调用与 x 所引用对象的实际类型最合适的那个类的方法。假设 x 的实际类型是 D,它是 C 类的子类。如果 D 类定义了方法 f(String,) 就直接调用它;否则, 将在 D 类的超类中寻找 f(String,) 以此类推。每次调用方法都要进行搜索,时间开销相当大。因此, 虚拟机预先为每个类创建了一个方法表(method table), 其中列出了所有方法的签名和实际调用的方法。这样一来,在真正调用方法的时候, 虚拟机仅查找这个表就行了。在前面的例子中, 虚拟机搜索 D 类的方法表, 以便寻找与调用 f(Sting) 相K配的方法。这个方法既有可能是 D.f(String), 也有可能是X.f(String), 这里的 X 是 D 的超类。这里需要提醒一点, 如果调用 super.f(param), 编译器将对隐式参数超类的方法表进行搜索。

现在, 查看一下程序清单 5-1 中调用 e.getSalary() 的详细过程。e 声明为 Employee 类型。Employee 类只有一个名叫 getSalary 的方法, 这个方法没有参数。 因此, 在这里不必担心重载解析的问题。

由于 getSalary 不是 private 方法、 static 方法或 final 方法,所以将采用动态绑定。虚拟机为 Employee 和 Manager 两个类生成方法表。在 Employee 的方法表中, 列出了这个类定义的所有方法:
           Employee:
           getNameO•> Employee.getNameO
           getSalaryO -> Employee.getSalaryO
           getHireDayO -> Employee.getHireDayO
           raiseSalary(double) -> Employee. raiseSal ary(doubl e)

实际上, 上面列出的方法并不完整, 稍后会看到 Employee 类有一个超类 Object,Employee 类从这个超类中还继承了许多方法,在此,我们略去了 Object 方法。

Manager 方法表稍微有些不同。其中有三个方法是继承而来的,一个方法是重新定义的,还有一个方法是新增加的。
           Manager:
           getNameO -> Employee.getNameO
           getSalaryO -> Manager.getSalary0
           getHireDayO -> Employee.getHireDayO
           raiseSalary(double) -> Employee.raiseSal ary(double)
           setBonus(double) -> Manager.setBonus(double)

在运行时, 调用 e.getSalaryO 的解析过程为:

1 ) 首先, 虚拟机提取 e 的实际类型的方法表。既可能是 Employee、 Manager 的方法表,也可能是 Employee 类的其他子类的方法表。

2 ) 接下来, 虚拟机搜索定义 getSalary 签名的类。此时, 虚拟机已经知道应该调用哪个方法。

3 ) 最后,虚拟机调用方法。动态绑定有一个非常重要的特性: 无需对现存的代码进行修改,就可以对程序进行扩展。假设增加一个新类 Executive, 并且变量 e 有可能引用这个类的对象, 我们不需要对包含调用e.getSalary() 的代码进行重新编译。 如果 e 恰好引用一个 Executive 类的对象,就会自动地调用 Executive.getSalaryO 方法。

警告: 在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。特别是, 如果超类方法是 public, 子类方法一定声明为 public。经常会发生这类错误:在声明子类方法的时候, 遗漏了 public 修饰符。此时,编译器将会把它解释为试图提供更严格的访问权限