不知道有没有小伙伴在面试时被问到过方法重写(Override)和重载(Overload)的区别?反正我是被问起过数次,大概情况是这样的:
面试官:说下Override和Overload的区别?
我:(内心:额,送人头的,然后就)方法重写发生在子、父继承的关系下,子类可以修改父类的方法,以达到增强、扩展等~~#¥%@ bala bala
面试官:嗯,还有吗?
我:(xx 一紧,还有啥?努力回想是不是忘说了啥)额,就这些吧
面试官:恩,今天面试就到这吧,你回去等消息吧
我:#¥%@~
方法重写和重载,相信只要是刚接触过java语言,对这两个概念就不会陌生,遇到相关的面试题估计也不少,今天我们就从面试题下手,然后再分析到其字节码层面,对这两个概念做一个介绍。
首先贴上面试题
// 父类
public class Parent {
int age = 40;
public void walk() {
System.out.println("parent walk");
}
}
// 子类
public class Child extends Parent {
int age = 15;
public void walk() {
System.out.println("child walk");
}
}
// 测试重载
public class TestOverload {
public void method(Parent parent) {
System.out.println("parent");
}
public void method(Child child) {
System.out.println("child");
}
public static void main(String[] args) {
TestOverload testOverload = new TestOverload();
Parent parent = new Child();
testOverload.method(parent);
Child child = new Child();
testOverload.method(child);
}
}
相信机智如你,早就知道了答案,结果:
parent
child
Process finished with exit code 0
不多解释概念,先javap看下字节码(为节省篇幅,只贴出部分常量池和main字节码)
Constant pool:
#1 = Methodref #12.#34 // java/lang/Object."<init>":()V
#2 = Fieldref #35.#36 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #22 // parent
#4 = Methodref #37.#38 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = String #25 // child
#6 = Class #39 // com/jvm/learnjvm/test/TestOverload
#7 = Methodref #6.#34 // com/jvm/learnjvm/test/TestOverload."<init>":()V
#8 = Class #40 // com/jvm/learnjvm/test/Child
#9 = Methodref #8.#34 // com/jvm/learnjvm/test/Child."<init>":()V
#10 = Methodref #6.#41 // com/jvm/learnjvm/test/TestOverload.method:(Lcom/jvm/learnjvm/test/Parent;)V
#11 = Methodref #6.#42 // com/jvm/learnjvm/test/TestOverload.method:(Lcom/jvm/learnjvm/test/Child;)V
#12 = Class #43 // java/lang/Object
#13 = Utf8 <init>
public static void main(java.lang.String[]); // main 方法
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #6 // class com/jvm/learnjvm/test/TestOverload
3: dup
4: invokespecial #7 // Method "<init>":()V
7: astore_1
8: new #8 // class com/jvm/learnjvm/test/Child
11: dup
12: invokespecial #9 // Method com/jvm/learnjvm/test/Child."<init>":()V
15: astore_2
16: aload_1
17: aload_2
18: invokevirtual #10 // Method method:(Lcom/jvm/learnjvm/test/Parent;)V
21: new #8 // class com/jvm/learnjvm/test/Child
24: dup
25: invokespecial #9 // Method com/jvm/learnjvm/test/Child."<init>":()V
28: astore_3
29: aload_1
30: aload_3
31: invokevirtual #11 // Method method:(Lcom/jvm/learnjvm/test/Child;)V
我们重点看一下main方法的 18: invokevirtual #10
和31: invokevirtual #11
,执行的方法,对照常量池的#10 = Methodref #6.#41 // com/jvm/learnjvm/test/TestOverload.method:(Lcom/jvm/learnjvm/test/Parent;)V
,
#11 = Methodref #6.#42 // com/jvm/learnjvm/test/TestOverload.method:(Lcom/jvm/learnjvm/test/Child;)V
,通过方法入参,可以看到很清楚的看到调用的方法情况,因为此时并没有运行,不知道将来传入的参数真实实例是什么,编译器只是根据声明的参数的类型和数量等匹配到合适的重载方法,这种方式,被称为“静态分派”。
同样在编译期确定的还有调用成员变量的经典面试题,有兴趣的可以自己看下字节码分析下
public class TestOverload {
public void method(Parent parent) {
System.out.println("parent");
}
public void method(Child child) {
System.out.println("child");
}
public static void main(String[] args) {
TestOverload testOverload = new TestOverload();
Parent parent = new Child();
System.out.println(parent.age);
Child child = new Child();
System.out.println(child.age);
}
}
结果
40
15
Process finished with exit code 0
分析完方法重载,现在分析下方法重写,先来一段大家都熟到不能再熟的代码(Parent和Child类依旧使用之前的)
public class TestOverride {
public static void main(String[] args) {
Parent parent = new Child();
parent.walk();
}
}
大家用脚指头想都知道的结果
child walk
Process finished with exit code 0
面对感觉理所应当的结果,我们还是先看看字节码吧
Constant pool:
#1 = Methodref #6.#22 // java/lang/Object."<init>":()V
#2 = Class #23 // com/jvm/learnjvm/test/Child
#3 = Methodref #2.#22 // com/jvm/learnjvm/test/Child."<init>":()V
#4 = Methodref #24.#25 // com/jvm/learnjvm/test/Parent.walk:()V
#5 = Class #26 // com/jvm/learnjvm/test/TestOverride
#6 = Class #27 // java/lang/Object
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/jvm/learnjvm/test/Child
3: dup
4: invokespecial #3 // Method com/jvm/learnjvm/test/Child."<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method com/jvm/learnjvm/test/Parent.walk:()V
12: return
执行方法调用的是9: invokevirtual #4
,指向的是常量池#4
,然后我们丝毫不慌的去常量看看,
#4 = Methodref #24.#25 // com/jvm/learnjvm/test/Parent.walk:()V
what???调用的是Parent的walk() ?气氛突然有些尴尬......
其实这个时候,就要说下面向对象的三大特性:封装、继承、多态中的多态的实现原理了。多态是什么不用我说大家也都知道,就不解释概念了,从字节码角度说一下,还是回到main方法的字节码再看一下,前面主要是创建对象,执行构造方法,并把当前创建的对象实例压到操作数栈,重点看下9,执行invokevirtual
指令,(jdk提供了5条方法调用的指令,在最下面有列出),invokevirtual
指令是找到当前操作数栈栈顶元素指向的对象的实际类型,也就是new出来的Child,然后执行该指令对应的常量池中的方法#4
,而这时候是运行期,jvm会根据方法的名称和描述来定位方法,调用的是Child实例的walk,这种动态调用方法的方式,也被称为动态分派。
方法调用指令
invokestatic
调用静态方法
invokevirtual
调用实例方法
invokespecial
调用私有方法、实例构造方法、super()
invokeinterface
调用引用类型为interface的实例方法
invokedynamic
JDK 7引入的,主要是为了支持动态语言的方法调用,如Lambda