我们先来看三个例题,如果都能答对的话,那么恭喜你,说明已经彻底理解了Java多态与重载、重写的本质。
例一: Java多态与重载
class Human {
}
class Man extends Human {
}
class Woman extends Human {
}
public class OverloadDemo {
public void say(Human human) {
System.out.println("say human");
}
//重载say()方法
public void say(Man man) {
System.out.println("say man");
}
//重载say()方法
public void say(Woman woman) {
System.out.println("say woman");
}
public static void main(String[] args) {
OverloadDemo demo = new OverloadDemo();
Human human = new Human();
Human man = new Man();
Human woman = new Woman();
demo.say(human);
demo.say(man);
demo.say(woman);
}
}
运行结果:
say human
say human
say human
例二 : Java多态与重写
class Human {
public void say() {
System.out.println("say human");
}
}
class Man extends Human {
//重写say()方法
public void say() {
System.out.println("say man");
}
}
class Woman extends Human {
//重写say()方法
public void say() {
System.out.println("say woman");
}
}
public class OverrideDemo {
public static void main(String[] args) {
Human man = new Man();
Human woman = new Human();
man.say();
woman.say();
man = new Woman();
man.say();
}
}
运行结果:
say man
say human
say woman
例三: Java多态与重载、重写
class Up {
}
class Down {
}
class Father {
public void say(Up up) {
System.out.println("father hands up!");
}
//重载say()方法
public void say(Down down) {
System.out.println("father hands down!");
}
}
class Son extends Father {
//重写say()方法
public void say(Up up) {
System.out.println("son hands up!");
}
public void say(Down down) {
System.out.println("son hands down!");
}
}
public class OverDemo {
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.say(new Up());
son.say(new Up());
}
}
运行结果:
father hands up!
son hands up!
我们以例三为例,说明一下Java虚拟机执行的过程。
在此之前先设定两个概念静态类型与实际类型,以这行代码为例“Father father = new Son();”,称前面的“Father”为变量的静态类型,后面的“Son”为实际类型。
编译阶段
首先是编译阶段,编译器会根据两点进行目标字节码的编译,一是方法调用者本身的静态类型是Father还是Son;二是方法参数的静态类型是Up还是Down。
下面是两行代码编译后的Java字节码。
//father.say(new Up());
16: aload_1
17: new #22 // class jvm/Over/Up
20: dup
21: invokespecial #24 // Method jvm/Over/Up."<init>":()V
24: invokevirtual #25 // Method jvm/Over/Father.say:(Ljvm/er/Up;)V
//son.say(new Up());
38: aload_2
39: new #22 // class jvm/Over/Up
42: dup
43: invokespecial #24 // Method jvm/Over/Up."<init>":()V
46: invokevirtual #25 // Method jvm/Over/Father.say:(Ljvm/er/Up;)V
其中16行到21行是在完成new up();的操作,38行到43行也是在完成new up();的操作。也就是说正真调用say()方法的字节码是一模一样的,(1)指令相同,都是invokevirtual指令;(2)参数相同,都是常量池中第25项的常量,而且注释显示了这个常量含义是Father.say()的符号引用,说的再明白点就是基本确定要调用Fahter类中的say()方法。
但最后的结果却不相同,这说明问题出在程序运行时,更真确地说出在invokevirtual指令的执行过程中。
运行时
invokevirtual指令执行的第一步就是确定运行期间方法调用者的实际类型,即Son;
第二步在Son类中找有没有和编译阶段确定的Father类中say()方法描述符、方法名都相符的方法,如果有,还要进行访问权限校验,若通过,就最终确定调用实际类型中的,若不通过,就抛出java.lang.IllegalAccessError异常;
第三步,如果在第二步的实际类型中没有找到相符的方法,则按照继承关系从下往上依次查找各个父类;
第四步,若始终没有找到,则抛出java.lang.AbstractMethodError异常。
思考题
如果我们把Son类中的“public void say(Up up)”方法删除,其他不变,执行结果会是什么呢?
答:根据运行时invokevirtual指令的执行过程,第二步就走不通了,此时就会来到第三步,按照继承关系从Son类的父类中寻找,那么运行结果就是“father hands up!”了。有兴趣的可以亲自动手敲一下。
附录:
例三中main()方法的全部字节码
public static void main(java.lang.String[]);
Code:
//Father father = new Father();
0: new #16 // class jvm/Over/Father
3: dup
4: invokespecial #18 // Method jvm/Over/Father."<init>":()V
7: astore_1
//Father son = new Son();
8: new #19 // class jvm/Over/Son
11: dup
12: invokespecial #21 // Method jvm/Over/Son."<init>":()V
15: astore_2
//father.say(new Up());
16: aload_1
17: new #22 // class jvm/Over/Up
20: dup
21: invokespecial #24 // Method jvm/Over/Up."<init>":()V
24: invokevirtual #25 // Method jvm/Over/Father.say:(Ljvm/Over/Up;)V
//son.say(new Up());
27: aload_2
28: new #22 // class jvm/Over/Up
31: dup
32: invokespecial #24 // Method jvm/Over/Up."<init>":()V
35: invokevirtual #25 // Method jvm/Over/Father.say:(Ljvm/Over/Up;)V
38: return