多態(tài)(Polymorphism)屬于面向?qū)ο笕筇卣髦唬那疤崾欠庋b形成獨(dú)立體,獨(dú)立體之間存在繼承關(guān)系,從而產(chǎn)生多態(tài)機(jī)制。多態(tài)是同一個(gè)行為具有多個(gè)不同表現(xiàn)形式或形態(tài)的能力。現(xiàn)實(shí)中,比如我們按下 F1 鍵這個(gè)動(dòng)作:
● 如果當(dāng)前在 Flash 界面下彈出的就是 AS 3 的幫助文檔;
● 如果當(dāng)前在 Word 下彈出的就是 Word 幫助;
● 如果當(dāng)前在 Windows 下彈出的就是 Windows 幫助和支持。
多態(tài)就是“同一個(gè)行為”發(fā)生在“不同的對(duì)象上”會(huì)產(chǎn)生不同的效果。那么在java中多態(tài)是如何體現(xiàn)的呢?
在java中允許這樣的兩種語(yǔ)法出現(xiàn),一種是向上轉(zhuǎn)型(Upcasting),一種是向下轉(zhuǎn)型(Downcasting),向上轉(zhuǎn)型是指子類(lèi)型轉(zhuǎn)換為父類(lèi)型,又被稱為自動(dòng)類(lèi)型轉(zhuǎn)換,向下轉(zhuǎn)型是指父類(lèi)型轉(zhuǎn)換為子類(lèi)型,又被稱為強(qiáng)制類(lèi)型轉(zhuǎn)換。請(qǐng)看下圖:
圖13-4:向上轉(zhuǎn)型和向下轉(zhuǎn)型
在java語(yǔ)言中有這樣的一個(gè)規(guī)定,無(wú)論是向上轉(zhuǎn)型還是向下轉(zhuǎn)型,兩種類(lèi)型之間必須要有繼承關(guān)系,沒(méi)有繼承關(guān)系情況下進(jìn)行向上轉(zhuǎn)型或向下轉(zhuǎn)型的時(shí)候編譯器都會(huì)報(bào)錯(cuò),這一點(diǎn)要死記硬背哦!
接下來(lái)我們來(lái)看一段代碼:
public class Animal {
public void move(){
System.out.println("Animal move!");
}
}
public class Cat extends Animal{
//方法覆蓋
public void move(){
System.out.println("走貓步!");
}
//子類(lèi)特有
public void catchMouse(){
System.out.println("抓老鼠!");
}
}
public class Bird extends Animal{
//方法覆蓋
public void move(){
System.out.println("鳥(niǎo)兒在飛翔!");
}
//子類(lèi)特有
public void sing(){
System.out.println("鳥(niǎo)兒在歌唱!");
}
}
public class Test01 {
public static void main(String[] args) {
//創(chuàng)建Animal對(duì)象
Animal a = new Animal();
a.move();
//創(chuàng)建Cat對(duì)象
Cat c = new Cat();
c.move();
//創(chuàng)建鳥(niǎo)兒對(duì)象
Bird b = new Bird();
b.move();
}
}
運(yùn)行結(jié)果如下圖所示:
圖13-5:運(yùn)行結(jié)果
其實(shí)在java中還允許這樣寫(xiě)代碼,請(qǐng)看:
public class Test02 {
public static void main(String[] args) {
Animal a1 = new Cat();
a1.move();
Animal a2 = new Bird();
a2.move();
}
}
運(yùn)行結(jié)果如下圖所示:
圖13-6:運(yùn)行結(jié)果
以上程序演示的就是多態(tài),多態(tài)就是“同一個(gè)行為(move)”作用在“不同的對(duì)象上”會(huì)有不同的表現(xiàn)結(jié)果。java中之所以有多態(tài)機(jī)制,是因?yàn)閖ava允許一個(gè)父類(lèi)型的引用指向一個(gè)子類(lèi)型的對(duì)象。也就是說(shuō)允許這種寫(xiě)法:Animal a2 = new Bird(),因?yàn)锽ird is a Animal是能夠說(shuō)通的。其中Animal a1 = new Cat()或者Animal a2 = new Bird()都是父類(lèi)型引用指向了子類(lèi)型對(duì)象,都屬于向上轉(zhuǎn)型(Upcasting),或者叫做自動(dòng)類(lèi)型轉(zhuǎn)換。
我來(lái)解釋一下這段代碼片段【Animal a1 = new Cat();a1.move(); 】:java程序包括編譯和運(yùn)行兩個(gè)階段,分析java程序一定要先分析編譯階段,然后再分析運(yùn)行階段,在編譯階段編譯器只知道a1變量的數(shù)據(jù)類(lèi)型是Animal,那么此時(shí)編譯器會(huì)去Animal.class字節(jié)碼中查找move()方法,發(fā)現(xiàn)Animal.class字節(jié)碼中存在move()方法,然后將該move()方法綁定到a1引用上,編譯通過(guò)了,這個(gè)過(guò)程我們可以理解為“靜態(tài)綁定”階段完成了。緊接著程序開(kāi)始運(yùn)行,進(jìn)入運(yùn)行階段,在運(yùn)行的時(shí)候?qū)嶋H上在堆內(nèi)存中new的對(duì)象是Cat類(lèi)型,也就是說(shuō)真正在move移動(dòng)的時(shí)候,是Cat貓對(duì)象在移動(dòng),所以運(yùn)行的時(shí)候就會(huì)自動(dòng)執(zhí)行Cat類(lèi)當(dāng)中的move()方法,這個(gè)過(guò)程可以稱為“動(dòng)態(tài)綁定”。但無(wú)論是什么時(shí)候,必須先“靜態(tài)綁定”成功之后才能進(jìn)入“動(dòng)態(tài)綁定”階段。
來(lái)看以下的一段代碼以及編譯結(jié)果:
public class Test03 {
public static void main(String[] args) {
Animal a = new Cat();
a.catchMouse();
}
}
編譯結(jié)果:
圖13-7:編譯錯(cuò)誤信息
有人認(rèn)為Cat貓是可以抓老鼠的呀,為什么會(huì)編譯報(bào)錯(cuò)呢?那是因?yàn)?ldquo;Animal a = new Cat();”在編譯的時(shí)候,編譯器只知道a變量的數(shù)據(jù)類(lèi)型是Animal,也就是說(shuō)它只會(huì)去Animal.class字節(jié)碼中查找catchMouse()方法,結(jié)果沒(méi)找到,自然“靜態(tài)綁定”就失敗了,編譯沒(méi)有通過(guò)。就像以上描述的錯(cuò)誤信息一樣:在類(lèi)型為Animal的變量a中找不到方法catchMouse()。
那么,假如說(shuō)我就是想讓這只貓去抓老鼠,以上代碼應(yīng)該如何修改呢?請(qǐng)看以下代碼:
public class Test04 {
public static void main(String[] args) {
//向上轉(zhuǎn)型
Animal a = new Cat();
//向下轉(zhuǎn)型:為了調(diào)用子類(lèi)對(duì)象特有的方法
Cat c = (Cat)a;
c.catchMouse();
}
}
運(yùn)行結(jié)果如下圖所示:
圖13-8:向下轉(zhuǎn)型
我們可以看到直接使用a引用是無(wú)法調(diào)用catchMouse()方法的,因?yàn)檫@個(gè)方法屬于子類(lèi)Cat中特有的行為,不是所有Animal動(dòng)物都可以抓老鼠的,要想讓它去抓老鼠,就必須做向下轉(zhuǎn)型(Downcasting),也就是使用強(qiáng)制類(lèi)型轉(zhuǎn)換將Animal類(lèi)型的a引用轉(zhuǎn)換成Cat類(lèi)型的引用c(Cat c = (Cat)a;),使用Cat類(lèi)型的c引用調(diào)用catchMouse()方法。
通過(guò)這個(gè)案例,可以得出:只有在訪問(wèn)子類(lèi)型中特有數(shù)據(jù)的時(shí)候,需要先進(jìn)行向下轉(zhuǎn)型。其實(shí)向下轉(zhuǎn)型就是用在這種情形之下。那么向下轉(zhuǎn)型會(huì)存在什么風(fēng)險(xiǎn)嗎?請(qǐng)看以下代碼:
public class Test05 {
public static void main(String[] args) {
Animal a = new Bird();
Cat c = (Cat)a;
}
}
以上代碼可以編譯通過(guò)嗎?答案是可以的,為什么呢?那是因?yàn)榫幾g器只知道a變量是Animal類(lèi)型,Animal類(lèi)和Cat類(lèi)之間存在繼承關(guān)系,所以可以進(jìn)行向下轉(zhuǎn)型(前面提到過(guò),只要兩種類(lèi)型之間存在繼承關(guān)系,就可以進(jìn)行向上或向下轉(zhuǎn)型),語(yǔ)法上沒(méi)有錯(cuò)誤,所以編譯通過(guò)了。但是運(yùn)行的時(shí)候會(huì)出問(wèn)題嗎,因?yàn)楫吘筧引用指向的真實(shí)對(duì)象是一只小鳥(niǎo)。來(lái)看運(yùn)行結(jié)果:
圖13-9:類(lèi)型轉(zhuǎn)換異常
以上的異常是很常見(jiàn)的ClassCastException,翻譯為類(lèi)型轉(zhuǎn)換異常,這種異常通常出現(xiàn)在向下轉(zhuǎn)型的操作過(guò)程當(dāng)中,當(dāng)類(lèi)型不兼容的情況下進(jìn)行轉(zhuǎn)型出現(xiàn)的異常,之所以出現(xiàn)此異常是因?yàn)樵诔绦蜻\(yùn)行階段a引用指向的對(duì)象是一只小鳥(niǎo),然后我們要將一只小鳥(niǎo)轉(zhuǎn)換成一只貓,這顯然是不合理的,因?yàn)樾▲B(niǎo)和貓之間是沒(méi)有繼承關(guān)系的。為了避免這種異常的發(fā)生,建議在進(jìn)行向下轉(zhuǎn)型之前進(jìn)行運(yùn)行期類(lèi)型判斷,這就需要我們學(xué)習(xí)一個(gè)運(yùn)算符了,它就是instanceof。
instanceof運(yùn)算符的語(yǔ)法格式是這樣的:
(引用 instanceof 類(lèi)型)
instanceof運(yùn)算符的運(yùn)算結(jié)果是布爾類(lèi)型,可能是true,也可能是false,假設(shè)(c instanceof Cat)結(jié)果是true則表示在運(yùn)行階段c引用指向的對(duì)象是Cat類(lèi)型,如果結(jié)果是false則表示在運(yùn)行階段c引用指向的對(duì)象不是Cat類(lèi)型。有了instanceof運(yùn)算符,向下轉(zhuǎn)型就可以這樣寫(xiě)了:
public class Test05 {
public static void main(String[] args) {
Animal a = new Bird();
if(a instanceof Cat){
Cat c = (Cat)a;
c.catchMouse();
}
}
}
以上程序運(yùn)行之后不再發(fā)生異常,并且什么也沒(méi)有輸出,那是因?yàn)閕f語(yǔ)句的條件并沒(méi)有成立,因?yàn)樵谶\(yùn)行階段a引用指向的對(duì)象不是Cat類(lèi)型,所以(a instanceof Cat)是false,自然就不會(huì)進(jìn)行向下轉(zhuǎn)型了,也不會(huì)出現(xiàn)ClassCastException異常了。在實(shí)際開(kāi)發(fā)中,java中有這樣一條默認(rèn)的規(guī)范需要大家記住:在進(jìn)行任何向下轉(zhuǎn)型的操作之前,要使用instanceof進(jìn)行判斷,這是一個(gè)很好的編程習(xí)慣。就像下面的代碼:
public class Test05 {
public static void main(String[] args) {
Animal a = new Bird();
if(a instanceof Cat){
Cat c = (Cat)a;
c.catchMouse();
}else if(a instanceof Bird){
Bird b = (Bird)a;
b.sing();
}
}
}
運(yùn)行結(jié)果如下圖所示:
圖13-10:向下轉(zhuǎn)型前判斷
到這里大家理解什么是多態(tài)了嗎?其實(shí)多態(tài)存在的三個(gè)必要條件分別是:
● 繼承
● 方法覆蓋
● 父類(lèi)型引用指向子類(lèi)型對(duì)象
多態(tài)顯然是離不開(kāi)方法覆蓋機(jī)制的,多態(tài)就是因?yàn)榫幾g階段綁定父類(lèi)當(dāng)中的方法,程序運(yùn)行階段自動(dòng)調(diào)用子類(lèi)對(duì)象上的方法,如果子類(lèi)對(duì)象上的方法沒(méi)有進(jìn)行重寫(xiě),這個(gè)時(shí)候創(chuàng)建子類(lèi)對(duì)象就沒(méi)有意義了,自然多態(tài)也就沒(méi)有意義了,只有子類(lèi)將方法重寫(xiě)之后調(diào)用到子類(lèi)對(duì)象上的方法產(chǎn)生不同效果時(shí),多態(tài)就形成了。實(shí)際上方法覆蓋機(jī)制和多態(tài)機(jī)制是捆綁的,誰(shuí)也離不開(kāi)誰(shuí),多態(tài)離不開(kāi)方法覆蓋,方法覆蓋離開(kāi)了多態(tài)也就沒(méi)有意義了。
接下里就來(lái)看看之前沒(méi)有解決的問(wèn)題:方法覆蓋主要是說(shuō)實(shí)例方法,靜態(tài)方法為什么不談方法覆蓋?
public class OverrideTest {
public static void main(String[] args) {
Math.sum();
MathSubClass.sum();
}
}
public class Math{
public static void sum(){
System.out.println("Math's sum execute!");
}
}
public class MathSubClass extends Math{
//嘗試覆蓋從父類(lèi)中繼承過(guò)來(lái)的靜態(tài)方法
public static void sum(){
System.out.println("MathSubClass's sum execute!");
}
}
運(yùn)行結(jié)果如下圖所示:
圖13-11:嘗試覆蓋靜態(tài)方法
我們發(fā)現(xiàn)貌似也發(fā)生了覆蓋,在程序運(yùn)行的時(shí)候確實(shí)也調(diào)用了“子類(lèi)MathSubClass”的sum方法,但這種“覆蓋”有意義嗎?其實(shí)上面的課程我們已經(jīng)說(shuō)過(guò)了,方法覆蓋和多態(tài)機(jī)制聯(lián)合起來(lái)才有意義,我們來(lái)看看這種“覆蓋”是否能夠達(dá)到“多態(tài)”的效果,請(qǐng)看代碼:
public class OverrideTest {
public static void main(String[] args) {
Math m = new MathSubClass();
m.sum();
m = null;
m.sum();
}
}
運(yùn)行結(jié)果如下圖所示:
圖13-12:運(yùn)行結(jié)果
通過(guò)以上的代碼,我們發(fā)現(xiàn)雖然創(chuàng)建了子類(lèi)型對(duì)象“new MathSubClass()”,但是程序在運(yùn)行的時(shí)候仍然調(diào)用的是Math類(lèi)當(dāng)中的sum方法,甚至m = null的時(shí)候再去調(diào)用m.sum()也沒(méi)有出現(xiàn)空指針異常,這說(shuō)明靜態(tài)方法的執(zhí)行壓根和對(duì)象無(wú)關(guān),既然和對(duì)象無(wú)關(guān)那就表示和多態(tài)無(wú)關(guān),既然和多態(tài)無(wú)關(guān),也就是說(shuō)靜態(tài)方法的“覆蓋”是沒(méi)有意義的,所以通常我們不談靜態(tài)方法的覆蓋。