多態(tài)我們在初期學習的時候就有所了解這個概念,但是要說深入的了解恐怕沒有幾個人诅岩。
什么是多態(tài)声离?
簡而言之就是同一個行為具有多個不同的表現(xiàn)形式或形態(tài)的能力。
比如說有一杯水锉走,不知道是溫的滨彻,涼的還是冰的燙的,但是通過手一摸就知道了挪蹭。摸水杯這個動作亭饵,對不同的水得到的結果是不同的。這就是多態(tài)梁厉。
那么辜羊,在JAVA中是怎么實現(xiàn)多態(tài)的呢?
public class Water {
public void showTem(){
System.out.println("我的溫度是: 0度");
}
}
public class IceWater extends Water {
public void showTem(){
System.out.println("我的溫度是: 0度");
}
}
public class WarmWater extends Water {
public void showTem(){
System.out.println("我的溫度是: 40度");
}
}
public class HotWater extends Water {
public void showTem(){
System.out.println("我的溫度是: 100度");
}
}
public class TestWater{
public static void main(String[] args) {
Water w = new WarmWater();
w.showTem();
w = new IceWater();
w.showTem();
w = new HotWater();
w.showTem();
}
}
//結果:
//我的溫度是: 40度
//我的溫度是: 0度
//我的溫度是: 100度
這里的方法showTem()就相當于你去摸水杯词顾。我們定義的water類型的引用變量w就相當于水杯八秃,你在水杯里放了什么溫度的水,那么我摸出來的感覺就是什么计技。就像代碼中的那樣喜德,放置不同溫度的水,得到的溫度也就不同垮媒,但水杯是同一個舍悯。
想必你也看出來了航棱,這段代碼中最關鍵的就是這一句
Water w = new WarmWater();
看過我前幾篇文章的應該知道,我說在講多態(tài)的時候萌衬,會講一個很重要的知識點 --- 向上轉型饮醇。這句代碼體現(xiàn)的就是向上轉型。后面我會詳細講解這一知識點秕豫。
多態(tài)的分類
已經(jīng)簡單的認識了多態(tài)了朴艰,那么我們來看一下多態(tài)的分類。
多態(tài)一般分為兩種:重寫式多態(tài)和重載式多態(tài)混移。重寫和重載這兩個知識點前面的文章已經(jīng)詳細將結果了祠墅,這里就不多說了。
重載式多態(tài)歌径,也叫編譯時多態(tài)毁嗦。也就是說這種多態(tài)再編譯時已經(jīng)確定好了。重載大家都知道回铛,方法名相同而參數(shù)列表不同的一組方法就是重載狗准。在調(diào)用這種重載的方法時,通過傳入不同的參數(shù)最后得到不同的結果茵肃。
多態(tài)的一般定義是:程序中定義的引用變量所指向的具體類型腔长,以及通過該引用變量發(fā)出的方法調(diào)用在編程時并不能確定,而是在程序運行期間才確定验残,這種情況叫做多態(tài)捞附。也即是重寫式多態(tài)的定義。
重寫式多態(tài)胚膊,也叫運行時多態(tài)故俐,這種多態(tài)通過動態(tài)綁定(dynamic binding)技術來實現(xiàn),是指在執(zhí)行期間判斷引用對象的實際類型紊婉,根據(jù)其實際的類型調(diào)用其相應的方法药版。也就是說只有程序運行起來,我們才能知道調(diào)用的是哪一個子類的方法喻犁。這種多態(tài)通過函數(shù)的重寫以及向上轉型來實現(xiàn)槽片。
動態(tài)綁定的技術涉及到了JVM技術,這個我暫時也還沒有去學習肢础,但是終歸是要學的还栓。
多態(tài)的條件:
繼承:在多態(tài)中必須存在有繼承關系的子類和父類
重寫:子類對父類中的某些方法進行重新定義,在調(diào)用這些方法時就會調(diào)用子類的方法传轰。
向上轉型:在多態(tài)中需要將子類的引用賦給父類對象剩盒,只有這樣該引用才能夠具備既能調(diào)用父類方法,
又能調(diào)用子類方法的能力慨蛙。
在這里辽聊,繼承也可以被替換為實現(xiàn)接口纪挎。
繼承和重寫我們在前面已經(jīng)討論過了,接下來可以看一下轉型是什么跟匆。
轉型又分向上轉型和向下轉型
向上轉型:
子類引用的對象轉換為父類類型稱為向上轉型异袄,通俗的說就是將子類對象轉換為父類對象,這里的父類可以是接口玛臂。
這里還是用代碼來舉例子:
public class Animal{
public void eat(){
System.out.println("animal eatting...");
}
}
public void Cat extends Animal{
public void eat(){
System.out.println("我吃魚");
}
}
public class Dog extends Animal{
public void eat(){
System.out.println("我吃骨頭");
}
public void run(){
System.out.println("我會跑");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Cat(); //向上轉型
animal.eat();
animal = new Dog(); //向上轉型
animal.eat();
}
}
//結果:
//我吃魚
//我吃骨頭
這就是向上轉型烤蜕,子類對象Cat,Dog都變成了父類的對象。但是實際上這個對象調(diào)用的方法還是子類的方法迹冤。
關于方法的調(diào)用順序后面還會重新講解讽营。
向上轉型中需要注意的問題是:
向上轉型后,子類自己獨有的方法會丟失叁巨。也就是說向上轉型僅能調(diào)用子類從父類那里繼承下來重寫的方法斑匪。比如上面Dog的run方法就無法通過轉型后的對象來調(diào)用。
向上轉型的好處:
減少重復的代碼锋勺,使代碼變得簡潔。提高系統(tǒng)擴展性狡蝶。
還是用代碼來說明問題∈鳎現(xiàn)在有很多動物要進行喂食,如果不使用向上轉型贪惹,那就需要給每一個動物單獨寫一個吃東西的方法苏章。
public void eat(Cat c){
c.eat();
}
public void eat(Dog d){
d.eat();
}
//.......
eat(new Cat());
eat(new Dog());
一種動物寫一個方法,如果我有一萬種動物奏瞬,我就要寫一萬個方法枫绅,寫完大概猴年馬月都過了好幾個了吧。好吧硼端,你很厲害并淋,你耐著性子寫完了,以為可以放松一會了珍昨,突然又來了一種新的動物县耽,你是不是又要單獨為它寫一個eat方法?開心了么镣典?
那如果我使用向上轉型呢兔毙?
我只需要這樣寫:
public void eat (Animal a){
a.eat();
}
eat(new Cat());
eat(new Dog());
//.....
恩,搞定了兄春。代碼是不是簡潔了許多挂洛?而且這個時候奥喻,如果我又有一種新的動物加進來,我只需要實現(xiàn)它自己的類微酬,讓他繼承Animal就可以了,而不需要為它單獨寫一個eat方法奴璃。是不是提高了擴展性?
向上轉型時,是子類轉換成父類對象甜癞,難道向下轉型就是父類對象轉換成子類對象嗎。其實不是這么簡單的宛乃。
向下轉型:
案例驅(qū)動
仔細看看上面的例子:
Animal a = new Cat();//向上轉型
Cat c = ((Cat) a);//向下轉型
c.eat();
//輸出 我吃魚
Dog d = ((Dog) a);
//報錯: java.lang.ClassCastException:Cat cannot be cast to Dog
Animal a1 = new Animal();
Cat c1 = ((Cat) a1);
c1.eat();
//報錯:java.lang.ClassCastException:Animal cannot be cast to Cat
為什么第一段代碼不報錯呢悠咱?相比你也知道了,因為a本身就是Cat對象征炼,所以它理所當然的可以向下轉型為Cat析既,也理所當然的不能轉為Dog,你見過一條狗突然就變成一只貓這種獵奇現(xiàn)象谆奥?
第二段代碼我們可以看到它是一個animal對象眼坏,它也不能被直接做轉型。
向下轉型注意事項:
向下轉型的前提是父類對象指向的是子類對象酸些,也就是向下轉型之前先要向上轉型宰译。并且向下轉型只能轉型為父類對象指向的那個子類對象,貓當然是不能變成狗的魄懂。
很多人就會又要奇怪了沿侈,我吃飽了撐得嗎?為什么要先向上轉型再向下轉型呢市栗?這時你翻上去看一下向上轉型那里的注意點缀拭,一個子類向上轉型后就沒法再訪問自己的獨立方法了,那如果狗狗利用向上轉型的高性能吃完飯后還想跑跑步消消食怎么辦呢填帽?
public void eat(Animal a){
if(a instanceof Dog){
Dog d = (Dog) a;
d.eat();
d.run();
}
if(a instanceof Cat){
Cat c = (Cat) a;
c.eat();
System.out.print("somthing");
}
a.eat();
}
eat(new Cat());
eat(new Dog());
//...
為了加深印象蛛淋,不妨再來看一個關于多態(tài)的經(jīng)典案例:
class A{
public String show(D obj){
return ("A and D");
}
public String show(A obj){
return ("A and A");
}
}
class B extends A{
public String show(B obj){
return ("B and B");
}
public String show(A obj){
return ("B and A");
}
}
class C extends B{
}
class D extends B{
}
public class Demo {
public static void main(String[] args) {
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));//1--B and B 錯誤了 A and A
System.out.println("2--" + a1.show(c));//2--A and A
System.out.println("3--" + a1.show(d));//3--"A and D"
System.out.println("4--" + a2.show(b));//4--"B and B" 錯誤了 B and A (B中沒辦法執(zhí)行自己獨有的方法)
System.out.println("5--" + a2.show(c));//5--"A and A" 錯誤了 B and A (同上)
System.out.println("6--" + a2.show(d));//6--"A and D"
System.out.println("7--" + b.show(b));//7--"B and B"
System.out.println("8--" + b.show(c));//8--"A and A" 錯誤了 B and B
System.out.println("9--" + b.show(d));//9--"A and D"
}
}
上面是我第一次的理解得出的結論,結果就是錯了一小半篡腌,這里就需要get一點新知識了褐荷。
當父類對象引用變量引用子類對象時,被引用對象的類型決定了調(diào)用誰的成員方法哀蘑,引用變量類型決定了可調(diào)用的方法诚卸。如果子類中沒有覆蓋該方法,那么會去父類中尋找绘迁。
為了解釋上面拗口的內(nèi)容合溺,這里舉一個簡單的例子:
class X{
public void show(Y y){
System.out.println("x and y");
}
public void show(){
System.out.println("only x");
}
}
class Y extends X {
public void show(Y y){
System.out.println("y and y");
}
public void show(int i){
}
}
class main{
public static void main(String[] args) {
X x = new Y();
x.show(new Y());//"y and y"
x.show();//"only x"
}
}
Y繼承了X,覆蓋了x中的show(Y y)方法,但是沒有覆蓋show()方法缀台。這個時候棠赛,引用類型為X的x指向的對象為Y,這個時候,調(diào)用的方法由Y決定睛约,會先從Y中尋找鼎俘。
執(zhí)行x.show(new Y());,該方法在Y中定義了辩涝,所以執(zhí)行的是Y里面的方法贸伐;
但是執(zhí)行x.show();的時候,有的人會說怔揩,Y中沒有這個方法白叫稀?它好像是去父類中找該方法了商膊,因為調(diào)用了X中的方法伏伐。
事實上,Y類中是有show()方法的晕拆,這個方法繼承自X藐翎,只不過沒有覆蓋該方法,所以沒有在Y中明確寫出來而已实幕,看起來像是調(diào)用了X中的方法吝镣,實際上調(diào)用的還是Y中的。
上面的是一個簡單的知識茬缩,它還不足以讓我們理解那個復雜的例子赤惊。我們再來看這樣一個知識:
繼承鏈中對象方法的調(diào)用的優(yōu)先級:this.show(O)、super.show(O)凰锡、this.show((super)O)、super.show((super)O)圈暗。
如果你能理解這個調(diào)用關系掂为,就ok了。我們回到那個復雜的例子:
a2.show(b) ==> this.show(b)员串,這里this指的是B勇哗。
然后.在B類中找show(B obj),找到了寸齐,可惜沒用欲诺,因為show(B obj)方法不在可調(diào)用范圍內(nèi),this.show(O)失敗渺鹦,進入下一級別:super.show(O)扰法,super指的是A。
在A 中尋找show(B obj)毅厚,失敗塞颁,因為沒用定義這個方法。進入第三級別:this.show((super)O),this指的是B祠锣。
在B中找show((A)O),找到了:show(A obj)酷窥,選擇調(diào)用該方法。
輸出:B and A
如果能照著這個思路將其他集中都總結清楚伴网,基本上多態(tài)就算是掌握了蓬推。
總結:
- 多態(tài)簡而言之就是同一個行為具有多個不同的表現(xiàn)形式或形態(tài)的能力。
- 運行時多態(tài)的前提:繼承(實現(xiàn))澡腾,重寫沸伏,向上轉型。
- 向上轉型與向下轉型蛋铆。
- 繼承鏈中對象方法的調(diào)用的優(yōu)先級:
this.show(O)馋评、super.show(O)、this.show((super)O)刺啦、super.show((super)O)留特。
關于多態(tài)還有一些其他的知識點,比如幾個關鍵字:static玛瘸、final等蜕青,我們在后面繼續(xù)學習。