不知道有沒有小伙伴在面試時被問到過方法重寫(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