Java有三大特性:封装、继承以及多态。很久以前,对于多态这一特性,我所了解的就是由方法的重载、重写所引起的。当一个对象调用一个方法时,我们会产生疑问,程序实际运行时调用的是哪一个方法?调用的是父类中的方法,还是子类本身的方法?如果一个类中有好几个重载方法,那么还涉及到选择调用哪一个重载方法版本。
现在从较深层次来理解Java的多态。
1、静态类型与实际类型
看下面的代码示例:
package xmh0511;
public class Test {
public static void main(String[] args) {
Test test = new Test();
Human man = new Man();
Human woman = new Woman();
test.sayHello(man);
test.sayHello(woman);
}
public void sayHello(Human human) {
System.out.println("I'm a Human");
}
public void sayHello(Man man) {
System.out.println("I'm a man");
}
public void sayHello(Woman woman) {
System.out.println("I'm a woman");
}
}
class Human {
}
class Man extends Human {
}
class Woman extends Human {
}
其中man的静态类型是Human,实际类型是Man;woman的静态类型是Human,实际类型是Woman。静态类型可以说是一个变量的声明类型,实际类型就是对象实例化时的具体类型。
2、重载方法版本的选择探讨
上面代码的执行结果是什么?程序是根据静态类型来匹配重载方法的版本,还是根据对象的实际类型来选择方法执行版本?
上面代码的执行结果是:
I'm a Human
I'm a Human
可见,它是根据静态类型来匹配的,即将实际参数(上面的man对象和woman对象)的静态类型(都是Human类型)与方法的参数类型进行匹配,所以该程序在调用sayHello方法时,选择执行的是sayHello(Human human)方法。
3、再想一个问题:重载方法版本的选择是在什么时候发生的?是在程序编译期(将源程序文件编译成字节码文件),还是在程序运行期(程序已经在虚拟机中处于运行状态)?
事实上,在编译器(如:Sun公司的javac编译器)编译源文件的时候,编译器能够事先确定实际参数的静态类型(如上面代码中,编译器能在编译的时候确定man和woman的静态类型就是Human),在编译成字节码文件的时候,就已经确定了要调用哪一个重载版本。重载方法版本的选择,是在编译期实现的。在字节码文件里,就已经确定了是调用哪一个重载方法。
4、重写方法版本的选择
4.1看下面的代码
public class DynanicTest {
public static void main(String[] args) {
DynanicTest dt = new DynanicTest();
Father father = new Father();
Father son = new Son();
father.sayHello();
son.sayHello();
}
}
class Father {
public void sayHello() {
System.out.println("Father sayHello()");
}
}
能看出,Son类继承了Father类,同时Son重写了父类中的sayHello()方法。
很容易就知道程序的执行结果是:
Father sayHello()
Son sayHello()
分析:
重写方法版本的选择过程大致如下:
首先得到该对象的Class对象,判断该对象的实际类型;
根据该类的继承关系,由下往上寻找与方法名和参数类型匹配的方法;
将这个方法调用的符号引用改为直接引用。
在上面的例子中:son对象调用sayHello()方法,首先从Son类中寻找是否存在sayHello()方法,如果找到了,则执行该方法,否则,到父类或者实现的接口中寻找是否存在sayHello()方法。
重写方法版本的选择是在程序运行时动态确定的,因为事先不知道一个对象的实际类型是什么,只有在程序运行时,才能确定一个对象真实的实际类型。