寫在前面
由于找工作的原因,最近幾個(gè)月都沒有更新博客了。。。這篇可能是今年最后一篇總結(jié)類的博文了,希望能夠?qū)懙暮命c(diǎn)吧 _
至此,博客中java基礎(chǔ)方面的內(nèi)容零零散散地也逐漸總結(jié)完了,基本上面試經(jīng)常問到的內(nèi)容都涵蓋了,希望大家喜歡
概念介紹
定義
多態(tài),是面向?qū)ο蟮某绦蛟O(shè)計(jì)語(yǔ)言最核心的特征。多態(tài),意味著一個(gè)對(duì)象有著多重特征,可以在特定的情況下,表現(xiàn)不同的狀態(tài),從而對(duì)應(yīng)著不同的屬性和方法。實(shí)現(xiàn)技術(shù)
動(dòng)態(tài)綁定(dynamic binding),是指在執(zhí)行期間判斷所引用對(duì)象的實(shí)際類型,根據(jù)其實(shí)際的類型調(diào)用其相應(yīng)的方法。作用
消除類型之間的耦合關(guān)系。應(yīng)用場(chǎng)景
現(xiàn)實(shí)中,關(guān)于多態(tài)的例子不勝枚舉。比方說按下 F1 鍵這個(gè)動(dòng)作,如果當(dāng)前在 Flash 界面下彈出的就是 AS 3 的幫助文檔;如果當(dāng)前在 Word 下彈出的就是 Word 幫助;在 Windows 下彈出的就是 Windows 幫助和支持。同一個(gè)事件發(fā)生在不同的對(duì)象上會(huì)產(chǎn)生不同的結(jié)果。
深入理解多態(tài)
多態(tài)存在的三個(gè)必要條件:
一、要有繼承;
二、要有重寫;
三、父類引用指向子類對(duì)象。
Java中多態(tài)的實(shí)現(xiàn)方式:
接口實(shí)現(xiàn)
繼承父類進(jìn)行方法重寫
同一個(gè)類中進(jìn)行方法重載
- 方法表與方法調(diào)用
Java 的方法調(diào)用有兩類,動(dòng)態(tài)方法調(diào)用與靜態(tài)方法調(diào)用。靜態(tài)方法調(diào)用是指對(duì)于類的靜態(tài)方法的調(diào)用方式,是靜態(tài)綁定的;而動(dòng)態(tài)方法調(diào)用需要有方法調(diào)用所作用的對(duì)象,是動(dòng)態(tài)綁定的。類調(diào)用 (invokestatic) 是在編譯時(shí)刻就已經(jīng)確定好具體調(diào)用方法的情況,而實(shí)例調(diào)用 (invokevirtual) 則是在調(diào)用的時(shí)候才確定具體的調(diào)用方法,這就是動(dòng)態(tài)綁定,也是多態(tài)要解決的核心問題。
方法表是動(dòng)態(tài)調(diào)用的核心,也是 Java 實(shí)現(xiàn)動(dòng)態(tài)調(diào)用的主要方式。它被存儲(chǔ)于方法區(qū)中的類型信息,包含有該類型所定義的所有方法及指向這些方法代碼的指針,注意這些具體的方法代碼可能是被覆寫的方法,也可能是繼承自基類的方法。
如有類定義 Person, Girl, Boy,
class Person {
public String toString(){
return "I'm a person.";
}
public void eat(){}
public void speak(){}
}
class Boy extends Person{
public String toString(){
return "I'm a boy";
}
public void speak(){}
public void fight(){}
}
class Girl extends Person{
public String toString(){
return "I'm a girl";
}
public void speak(){}
public void sing(){}
}
當(dāng)這三個(gè)類被載入到 Java 虛擬機(jī)之后,方法區(qū)中就包含了各自的類的信息。Girl 和 Boy 在方法區(qū)中的方法表可表示如下:

可以看到,Girl 和 Boy 的方法表包含繼承自 Object 的方法,繼承自直接父類 Person 的方法及各自新定義的方法。注意方法表?xiàng)l目指向的具體的方法地址,如 Girl 的繼承自 Object 的方法中,只有 toString() 指向自己的實(shí)現(xiàn)(Girl 的方法代碼),其余皆指向 Object 的方法代碼;其繼承自于 Person 的方法 eat() 和 speak() 分別指向 Person 的方法實(shí)現(xiàn)和本身的實(shí)現(xiàn)。
Person 或 Object 的任意一個(gè)方法,在它們的方法表和其子類 Girl 和 Boy 的方法表中的位置 (index) 是一樣的。這樣 JVM 在調(diào)用實(shí)例方法其實(shí)只需要指定調(diào)用方法表中的第幾個(gè)方法即可。
如調(diào)用如下:
class Party{
…
void happyHour(){
Person girl = new Girl();
girl.speak();
…
}
}
當(dāng)編譯 Party 類的時(shí)候,生成 girl.speak()的方法調(diào)用假設(shè)為:
Invokevirtual #12
設(shè)該調(diào)用代碼對(duì)應(yīng)著 girl.speak(); #12 是 Party 類的常量池的索引。JVM 執(zhí)行該調(diào)用指令的過程如下所示:

JVM 首先查看 Party 的常量池索引為 12 的條目(應(yīng)為 CONSTANT_Methodref_info 類型,可視為方法調(diào)用的符號(hào)引用),進(jìn)一步查看常量池(CONSTANT_Class_info,CONSTANT_NameAndType_info ,CONSTANT_Utf8_info)可得出要調(diào)用的方法是 Person 的 speak 方法(注意引用 girl 是其基類 Person 類型),查看 Person 的方法表,得出 speak 方法在該方法表中的偏移量 15(offset),這就是該方法調(diào)用的直接引用。
當(dāng)解析出方法調(diào)用的直接引用后(方法表偏移量 15),JVM 執(zhí)行真正的方法調(diào)用:根據(jù)實(shí)例方法調(diào)用的參數(shù) this 得到具體的對(duì)象(即 girl 所指向的位于堆中的對(duì)象),據(jù)此得到該對(duì)象對(duì)應(yīng)的方法表 (Girl 的方法表 ),進(jìn)而調(diào)用方法表中的某個(gè)偏移量所指向的方法(Girl 的 speak() 方法的實(shí)現(xiàn))。
-
接口調(diào)用
因?yàn)?Java 類是可以同時(shí)實(shí)現(xiàn)多個(gè)接口的,而當(dāng)用接口引用調(diào)用某個(gè)方法的時(shí)候,情況就有所不同了。Java 允許一個(gè)類實(shí)現(xiàn)多個(gè)接口,從某種意義上來說相當(dāng)于多繼承,這樣同樣的方法在基類和派生類的方法表的位置就可能不一樣了。
interface IDance{
void dance();
}
class Person {
public String toString(){
return "I'm a person.";
}
public void eat(){}
public void speak(){}
}
class Dancer extends Person
implements IDance {
public String toString(){
return "I'm a dancer.";
}
public void dance(){}
}
class Snake implements IDance{
public String toString(){
return "A snake.";
}
public void dance(){
//snake dance
}
}

可以看到,由于接口的介入,繼承自于接口 IDance 的方法 dance()在類 Dancer 和 Snake 的方法表中的位置已經(jīng)不一樣了,顯然我們無法通過給出方法表的偏移量來正確調(diào)用 Dancer 和 Snake 的這個(gè)方法。這也是 Java 中調(diào)用接口方法有其專有的調(diào)用指令(invokeinterface)的原因。
Java 對(duì)于接口方法的調(diào)用是采用搜索方法表的方式,對(duì)如下的方法調(diào)用
invokeinterface #13
JVM 首先查看常量池,確定方法調(diào)用的符號(hào)引用(名稱、返回值等等),然后利用 this 指向的實(shí)例得到該實(shí)例的方法表,進(jìn)而搜索方法表來找到合適的方法地址。
因?yàn)槊看谓涌谡{(diào)用都要搜索方法表,所以從效率上來說,接口方法的調(diào)用總是慢于類方法的調(diào)用的。
??紗栴}解析
下面來看一個(gè)關(guān)于多態(tài)的經(jīng)典實(shí)例
(一)相關(guān)類
public class A {
public String show(D obj) {
return ("A and D");
}
public String show(A obj) {
return ("A and A");
}
}
public class B extends A{
public String show(B obj){
return ("B and B");
}
public String show(A obj){
return ("B and A");
}
}
public class C extends B{
}
public class D extends B{
}
(二)問題:以下輸出結(jié)果是什么?
A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();
System.out.println("1--" + a1.show(b));
System.out.println("2--" + a1.show(c));
System.out.println("3--" + a1.show(d));
System.out.println("4--" + a2.show(b));
System.out.println("5--" + a2.show(c));
System.out.println("6--" + a2.show(d));
System.out.println("7--" + b.show(b));
System.out.println("8--" + b.show(c));
System.out.println("9--" + b.show(d));
(三)答案
1--A and A
2--A and A
3--A and D
4--B and A
5--B and A
6--A and D
7--B and B
8--B and B
9--A and D
(四)分析
①②③比較好理解,一般不會(huì)出錯(cuò)。④⑤就有點(diǎn)糊涂了,為什么輸出的不是"B and B”呢???!先來回顧一下多態(tài)性。
運(yùn)行時(shí)多態(tài)性是面向?qū)ο蟪绦蛟O(shè)計(jì)代碼重用的一個(gè)最強(qiáng)大機(jī)制,動(dòng)態(tài)性的概念也可以被說成“一個(gè)接口,多個(gè)方法”。Java實(shí)現(xiàn)運(yùn)行時(shí)多態(tài)性的基礎(chǔ)是動(dòng)態(tài)方法調(diào)度,它是一種在運(yùn)行時(shí)而不是在編譯期調(diào)用重載方法的機(jī)制。
方法的重寫Overriding和重載Overloading是Java多態(tài)性的不同表現(xiàn)。重寫Overriding是父類與子類之間多態(tài)性的一種表現(xiàn),重載Overloading是一個(gè)類中多態(tài)性的一種表現(xiàn)。如果在子類中定義某方法與其父類有相同的名稱和參數(shù),我們說該方法被重寫(Overriding)。子類的對(duì)象使用這個(gè)方法時(shí),將調(diào)用子類中的定義,對(duì)它而言,父類中的定義如同被“屏蔽”了。如果在一個(gè)類中定義了多個(gè)同名的方法,它們或有不同的參數(shù)個(gè)數(shù)或有不同的參數(shù)類型,則稱為方法的重載(Overloading)。Overloaded的方法是可以改變返回值的類型。
當(dāng)超類對(duì)象引用變量引用子類對(duì)象時(shí),被引用對(duì)象的類型而不是引用變量的類型決定了調(diào)用誰(shuí)的成員方法,但是這個(gè)被調(diào)用的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法。 (但是如果強(qiáng)制把超類轉(zhuǎn)換成子類的話,就可以調(diào)用子類中新添加而超類沒有的方法了。)
好了,先溫習(xí)到這里,言歸正傳!實(shí)際上這里涉及方法調(diào)用的優(yōu)先問題 ,優(yōu)先級(jí)由高到低依次為:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。讓我們來看看它是怎么工作的。
比如④,a2.show(b),a2是一個(gè)引用變量,類型為A,則this為a2,b是B的一個(gè)實(shí)例,于是它到類A里面找show(B obj)方法,沒有找到,于是到A的super(超類)找,而A沒有超類,因此轉(zhuǎn)到第三優(yōu)先級(jí)this.show((super)O),this仍然是a2,這里O為B,(super)O即(super)B即A,因此它到類A里面找show(A obj)的方法,類A有這個(gè)方法,但是由于a2引用的是類B的一個(gè)對(duì)象,B覆蓋了A的show(A obj)方法,因此最終鎖定到類B的show(A obj),輸出為"B and A”。
再比如⑧,b.show(c),b是一個(gè)引用變量,類型為B,則this為b,c是C的一個(gè)實(shí)例,于是它到類B找show(C obj)方法,沒有找到,轉(zhuǎn)而到B的超類A里面找,A里面也沒有,因此也轉(zhuǎn)到第三優(yōu)先級(jí)this.show((super)O),this為b,O為C,(super)O即(super)C即B,因此它到B里面找show(B obj)方法,找到了,由于b引用的是類B的一個(gè)對(duì)象,因此直接鎖定到類B的show(B obj),輸出為"B and B”。
按照上面的方法,可以正確得到其他的結(jié)果。
問題還要繼續(xù),現(xiàn)在我們?cè)賮砜瓷厦娴姆治鲞^程是怎么體現(xiàn)出藍(lán)色字體那句話的內(nèi)涵的。它說:當(dāng)超類對(duì)象引用變量引用子類對(duì)象時(shí),被引用對(duì)象的類型而不是引用變量的類型決定了調(diào)用誰(shuí)的成員方法,但是這個(gè)被調(diào)用的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法。還是拿a2.show(b)來說吧。
a2是一個(gè)引用變量,類型為A,它引用的是B的一個(gè)對(duì)象,因此這句話的意思是由B來決定調(diào)用的是哪個(gè)方法。因此應(yīng)該調(diào)用B的show(B obj)從而輸出"B and B”才對(duì)。但是為什么跟前面的分析得到的結(jié)果不相符呢?!問題在于我們不要忽略了藍(lán)色字體的后半部分,那里特別指明:這個(gè)被調(diào)用的方法必須是在超類中定義過的,也就是被子類覆蓋的方法。B里面的show(B obj)在超類A中有定義嗎?沒有!那就更談不上被覆蓋了。實(shí)際上這句話隱藏了一條信息:它仍然是按照方法調(diào)用的優(yōu)先級(jí)來確定的。它在類A中找到了show(A obj),如果子類B沒有覆蓋show(A obj)方法,那么它就調(diào)用A的show(A obj)(由于B繼承A,雖然沒有覆蓋這個(gè)方法,但從超類A那里繼承了這個(gè)方法,從某種意義上說,還是由B確定調(diào)用的方法,只是方法是在A中實(shí)現(xiàn)而已);現(xiàn)在子類B覆蓋了show(A obj),因此它最終鎖定到B的show(A obj)。這就是那句話的意義所在。
參考文獻(xiàn)
[1]Java多態(tài)性理解
[2]java提高篇之理解java的三大特性——多態(tài)
[3]java多態(tài)實(shí)現(xiàn)原理
[4]深入理解java多態(tài)性