Java 中的多態一般分爲兩種:重寫式多態和重載式多態。 |
重載式多態,也叫編譯時多態。也就是說這種多態再編譯時已經確定好了。重載大家都知道,方法名相同而參數列表不同的一組方法就是重載。在調用這種重載的方法時,通過傳入不同的參數最後得到不同的結果。
但是這裏是有歧義的,有的人覺得不應該把重載也算作多態。因爲很多人對多態的理解是:程序中定義的引用變量所指向的具體類型和通過該引用變量發出的方法調用在編程時並不確定,而是在程序運行期間才確定,這種情況叫做多態。 這個定義中描述的就是我們的第二種多態—重寫式多態。並且,重載式多態並不是面向對象編程特有的,而多態卻是面向對象三大特性之一(如果我說的不對,記得告訴我。。)。
我覺得大家也沒有必要在定義上去深究這些,我的理解是:同一個行爲具有多個不同表現形式或形態的能力就是多態,所以我認爲重載也是一種多態,如果你不同意這種觀點,我也接受。
重寫式多態,也叫運行時多態。這種多態通過動態綁定(dynamic binding)技術來實現,是指在執行期間判斷所引用對象的實際類型,根據其實際的類型調用其相應的方法。也就是說,只有程序運行起來,你才知道調用的是哪個子類的方法。 這種多態通過函數的重寫以及向上轉型來實現,我們上面代碼中的例子就是一個完整的重寫式多態。我們接下來講的所有多態都是重寫式多態,因爲它纔是面向對象編程中真正的多態。
子類引用的對象轉換爲父類類型稱爲向上轉型。通俗地說就是是將子類對象轉爲父類對象。此處父類對象可以是接口。
看一個大家都知道的例子:
實例
public class Animal { public void eat(){ System.out.println("animal eatting..."); } } public class 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(); } } //結果: //我喫魚 //我喫骨頭
這就是向上轉型,Animal animal = new Cat(); 將子類對象 Cat 轉化爲父類對象 Animal。這個時候 animal 這個引用調用的方法是子類方法。
轉型過程中需要注意的問題
- 向上轉型時,子類單獨定義的方法會丟失。比如上面Dog類中定義的run方法,當animal引用指向Dog類實例時是訪問不到run方法的,animal.run()會報錯。
- 子類引用不能指向父類對象。Cat c = (Cat)new Animal()這樣是不行的。
向上轉型的好處
- 減少重複代碼,使代碼變得簡潔。
- 提高系統擴展性。
與向上轉型相對應的就是向下轉型了。向下轉型是把父類對象轉爲子類對象。(請注意!這裏是有坑的。)
案例驅動
先看一個例子:
//還是上面的animal和cat dog Animal a = new Cat(); Cat c = ((Cat) a); c.eat(); //輸出 我喫魚 Dog d = ((Dog) a); d.eat(); // 報錯 : java.lang.ClassCastException:com.chengfan.animal.Cat cannot be cast to com.chengfan.animal.Dog Animal a1 = new Animal(); Cat c1 = ((Cat) a1); c1.eat(); // 報錯 : java.lang.ClassCastException:com.chengfan.animal.Animal cannot be cast to com.chengfan.animal.Cat
爲什麼第一段代碼不報錯呢?相比你也知道了,因爲 a 本身就是 Cat 對象,所以它理所當然的可以向下轉型爲 Cat,也理所當然的不能轉爲 Dog,你見過一條狗突然就變成一隻貓這種操蛋現象?
而 a1 爲 Animal 對象,它也不能被向下轉型爲任何子類對象。比如你去考古,發現了一個新生物,知道它是一種動物,但是你不能直接說,啊,它是貓,或者說它是狗。
向下轉型注意事項
- 向下轉型的前提是父類對象指向的是子類對象(也就是說,在向下轉型之前,它得先向上轉型)
- 向下轉型只能轉型爲本類對象(貓是不能變成狗的)。
看一個經典案例:
實例
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)); 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 //能看懂這個結果麼?先自分析一下。
前三個,強行分析,還能看得懂。但是第四個,大概你就傻了吧。爲什麼不是b and b呢?
這裏就要學點新東西了。
當父類對象引用變量引用子類對象時,被引用對象的類型決定了調用誰的成員方法,引用變量類型決定可調用的方法。如果子類中沒有覆蓋該方法,那麼會去父類中尋找。
可能讀起來比較拗口,我們先來看一個簡單的例子:
實例
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()); x.show(); } } //結果 //y and y //only x
Y 繼承了 X,覆蓋了 X 中的 show(Y y) 方法,但是沒有覆蓋 show() 方法。
這個時候,引用類型爲X的 x 指向的對象爲 Y,這個時候,調用的方法由 Y 決定,會先從 Y 中尋找。執行 x.show(new Y());,該方法在 Y 中定義了,所以執行的是 Y 裏面的方法;
但是執行 x.show(); 的時候,有的人會說,Y 中沒有這個方法啊?它好像是去父類中找該方法了,因爲調用了 X 中的方法。
事實上,Y 類中是有 show() 方法的,這個方法繼承自 X,只不過沒有覆蓋該方法,所以沒有在 Y 中明確寫出來而已,看起來像是調用了 X 中的方法,實際上調用的還是 Y 中的。
這個時候再看上面那句難理解的話就不難理解了吧。X是引用變量類型,它決定哪些方法可以調用;show()和 show(Y y) 可以調用,而 show(int i)不可以調用。Y 是被引用對象的類型,它決定了調用誰的方法:調用 y 的方法。
上面的是一個簡單的知識,它還不足以讓我們理解那個複雜的例子。我們再來看這樣一個知識:
繼承鏈中對象方法的調用的優先級:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。
如果你能理解這個調用關係,那麼多態你就掌握了。我們回到那個複雜的例子:
abcd 的關係是這樣的:C/D —> B —> A
我們先來分析4 : a2.show(b)
首先,a2是類型爲A的引用類型,它指向類型爲B的對象。A確定可調用的方法:show(D obj)和show(A obj)。
a2.show(b) ==> this.show(b),這裏this指的是B。
然後.在B類中找show(B obj),找到了,可惜沒用,因爲show(B obj)方法不在可調用範圍內,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),選擇調用該方法。
輸出:B and A
如果你能看懂這個過程,並且能分析出其他的情況,那你就真的掌握了。
我們再來看一下9:b.show(d)
首先,b爲類型爲B的引用對象,指向類型爲B的對象。沒有涉及向上轉型,只會調用本類中的方法。
在B中尋找show(D obj),方法。現在你不會說沒找到了吧?找到了,直接調用該方法。
輸出 A and D。
本篇文章的內容大體上就是這些了。我們來總結一下。
- 多態,簡而言之就是同一個行爲具有多個不同表現形式或形態的能力。
- 多態的分類:運行時多態和編譯時多態。
- 運行時多態的前提:繼承(實現),重寫,向上轉型
- 向上轉型與向下轉型。
- 繼承鏈中對象方法的調用的優先級:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。