Java面向物件之多型(向上轉型與向下轉型)
多型,大概每個人都知道。但是,又有幾個人真的理解什麼是多型、多型有哪些細節呢?如果你看到這篇文章的名字,腦海中對多型沒有一個清晰的概念,不妨點進來看看,也許會有收穫。
什麼是多型
簡單的理解多型
多型,簡而言之就是同一個行為具有多個不同表現形式或形態的能力。比如說,有一杯水,我不知道它是溫的、冰的還是燙的,但是我一摸我就知道了。我摸水杯這個動作,對於不同溫度的水,就會得到不同的結果。這就是多型。
那麼,java中是怎麼體現多型呢?我們來直接看程式碼:
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();
看過我前幾篇文章的應該知道,我說在講多型的時候,會講一個很重要的知識點 — 向上轉型。
這句程式碼體現的就是向上轉型。後面我會詳細講解這一知識點。
多型的分類
已經簡單的認識了多型了,那麼我們來看一下多型的分類。
多型一般分為兩種:重寫式多型和過載式多型
過載式多型,也叫編譯時多型。也就是說這種多型再編譯時已經確定好了。過載大家都知道,方法名相同而引數列表不同的一組方法就是過載。在呼叫這種過載的方法時,通過傳入不同的引數最後得到不同的結果。
但是這裡是有歧義的,有的人覺得不應該把過載也算作多型。因為很多人對多型的理解是:程式中定義的引用變數所指向的具體型別和通過該引用變數發出的方法呼叫在程式設計時並不確定,而是在程式執行期間才確定,這種情況叫做多型。 這個定義中描述的就是我們的第二種多型—重寫式多型。並且,過載式多型並不是面向物件程式設計特有的,而多型卻是面向物件三大特性之一(如果我說的不對,記得告訴我。。)。
我覺得大家也沒有必要在定義上去深究這些,我的理解是:同一個行為具有多個不同表現形式或形態的能力就是多型,所以我認為過載也是一種多型,如果你不同意這種觀點,我也接受。
重寫式多型,也叫執行時多型。這種多型通過動態繫結(dynamic binding)技術來實現,是指在執行期間判斷所引用物件的實際型別,根據其實際的型別呼叫其相應的方法。也就是說,只有程式執行起來,你才知道呼叫的是哪個子類的方法。
這種多型通過函式的重寫以及向上轉型來實現,我們上面程式碼中的例子就是一個完整的重寫式多型。我們接下來講的所有多型都是重寫式多型,因為它才是面向物件程式設計中真正的多型。動態繫結技術涉及到jvm,暫時不講(因為我也不懂,哈哈哈哈),感興趣的可以自己去研究一下,我暫時還沒有時間去研究jvm。。
多型的條件
前面說過,我們接下來說的多型,都是執行時多型。
- 繼承。在多型中必須存在有繼承關係的子類和父類。
- 重寫。子類對父類中某些方法進行重新定義,在呼叫這些方法時就會呼叫子類的方法。
- 向上轉型。在多型中需要將子類的引用賦給父類物件,只有這樣該引用才能夠具備技能呼叫父類的方法和子類的方法。
繼承也可以替換為實現介面。
繼承和重寫之前都說過了,接下來我們來看一下轉型是什麼。
向上轉型與向下轉型
向上轉型
子類引用的物件轉換為父類型別稱為向上轉型。通俗地說就是是將子類物件轉為父類物件。此處父類物件可以是介面。
案例驅動
看一個大家都知道的例子:
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()
這樣是不行的。
向上轉型的好處
- 減少重複程式碼,使程式碼變得簡潔。
- 提高系統擴充套件性。
舉個例子:比如我現在有很多種類的動物,要餵它們吃東西。如果不用向上轉型,那我需要這樣寫:
public void eat(Cat c){
c.eat();
}
public void eat(Dog d){
d.eat();
}
//......
eat(new Cat());
eat(new Cat());
eat(new Dog());
//......
一種動物寫一個方法,如果我有一萬種動物,我就要寫一萬個方法,寫完大概猴年馬月都過了好幾個了吧。好吧,你很厲害,你耐著性子寫完了,以為可以放鬆一會了,突然又來了一種新的動物,你是不是又要單獨為它寫一個eat方法?開心了麼?
那如果我使用向上轉型呢?我只需要這樣寫:
public void eat(Animal a){
a.eat();
}
eat(new Cat());
eat(new Cat());
eat(new Dog());
//.....
恩,搞定了。程式碼是不是簡潔了許多?而且這個時候,如果我又有一種新的動物加進來,我只需要實現它自己的類,讓他繼承Animal就可以了,而不需要為它單獨寫一個eat方法。是不是提高了擴充套件性?
向下轉型
與向上轉型相對應的就是向下轉型了。向下轉型是把父類物件轉為子類物件。(請注意!這裡是有坑的。)
案例驅動
先看一個例子:
//還是上面的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物件,它也不能被向下轉型為任何子類物件。比如你去考古,發現了一個新生物,知道它是一種動物,但是你不能直接說,啊,它是貓,或者說它是狗。
向下轉型注意事項
- 向下轉型的前提是父類物件指向的是子類物件(也就是說,在向下轉型之前,它得先向上轉型)
向下轉型只能轉型為本類物件(貓是不能變成狗的)。
大概你會說,我特麼有病啊,我先向上轉型再向下轉型??
我們回到上面的問題:餵動物吃飯,吃了飯做點什麼呢?不同的動物肯定做不同的事,怎麼做呢?
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.println("我也想跑,但是不會"); //貓會抱怨 } a.eat();//其他動物只會吃 } eat(new Cat()); eat(new Cat()); eat(new Dog()); //.....
現在,你懂了麼?這就是向下轉型的簡單應用,可能舉得例子不恰當,但是也可以說明一些問題。
敲黑板,劃重點!看到那個instanceof了麼?
經典案例分析多型
基本的多型和轉型我們都會了,最後加點餐。看一個經典案例:
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)。