java-物件導向程式設計--多型

Fysddsw_lc發表於2018-03-21

繼承是為了重用父類程式碼。兩個類若存在IS-A的關係就可以使用繼承。同時繼承也為實現多型做了鋪墊。

多型

多型就是指程式中定義的引用變數所指向的具體型別和通過該引用變數發出的方法呼叫在程式設計時並不確定,而是在程式執行期間才確定,即一個引用變數倒底會指向哪個類的例項物件,該引用變數發出的方法呼叫到底是哪個類中實現的方法,必須在由程式執行期間才能決定。因為在程式執行時才確定具體的類,這樣,不用修改源程式程式碼,就可以讓引用變數繫結到各種不同的類實現上,從而導致該引用呼叫的具體方法隨之改變,即不修改程式程式碼就可以改變程式執行時所繫結的具體程式碼,讓程式可以選擇多個執行狀態,這就是多型性。

多型,簡而言之就是同一個行為具有多個不同表現形式或形態的能力。比如說,有一杯水,我不知道它是溫的、冰的還是燙的,但是我一摸我就知道了。我摸水杯這個動作,對於不同溫度的水,就會得到不同的結果。這就是多型。

多型的條件

  1. 繼承。在多型中必須存在有繼承關係的子類和父類。
  2. 重寫。子類對父類的默些方法重新定義,在呼叫這些方法的時候就會呼叫子類的方法。
  3. 向上轉型。在多型中需要將子類的引用賦值給父類物件,只有這樣該引用才能具備技能呼叫父類的方法和子類的方法。
只有滿足了上述三個條件,我們才能實現多型。

對於java而言,多型的實現機制遵循一個原則:當父類物件引用變數引用子類物件時,被引用物件的型別而不是引用變數的型別決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在父類中定義過的,也就是被子類覆蓋的方法。

向上轉型

子類引用的物件轉換成父類型別成為向上轉型。通俗的說就是將子類物件轉成父類物件。此處父類物件可以是介面。

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 = new Dog();//向上轉型
animal.eat();
}

}複製程式碼

輸出:狗吃骨頭複製程式碼

這就是向上轉型,Animal animal = new Dog();將子類物件Dog轉化為父類物件Animal。這個時候animal這個引用呼叫的方法是子類方法。

轉型過程中需要注意的問題

  • 向上轉型時,子類單獨定義的方法會丟失。比如上面Dog類中定義的run方法,當animal引用指向Dog類例項時是訪問不到run方法的,animal.run()會報錯。
  • 子類引用不能指向父類物件。Dog d = (Dog)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,因為是Cat所以不能轉為Dog。

而a1是Anmail物件,它不能向下轉型Wie任何子類物件。比如發現一個古生物化石,知道它是一種動物,但你不能直接說他是貓或者他是狗。

向下轉型注意事項

  • 向下轉型的前提是父類物件指向的是子類物件(也就是說,在向下轉型之前,它得先向上轉型)
  • 向下轉型只能轉型為本類物件(貓是不能變成狗的)。

instanceof 關鍵字:instanceof是Javaphp的一個二元操作符(運算子),和==,>,<是同一類東西。由於它是由字母組成的,所以也是Java的保留關鍵字。它的作用是判斷其左邊物件是否為其右邊類的例項,返回boolean型別的資料。可以用來判斷繼承中的子類的例項是否為父類的實現。

instanceof一般放在型別轉換的前面,合理規避異常產生!

public void eat(Animal a){
/* 
         * 向下轉型(強制轉型) 
         * 子類引用指向父類的例項(物件),此處必須今習慣強制型別轉換 
         * 該例項可以呼叫子類特有的方法 
         * 必須滿足轉型條件才能轉換(),子類間不能隨意強制轉換,但是子類引用指向父類例項,可以強制轉換 
         * instanceof運算子:返回true/false, 
 */  

    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());複製程式碼

花木蘭替父從軍

向下轉型花木蘭替父親花弧從軍。那麼這時候花木蘭是子類,花弧是父類。花弧有自己的成員屬性年齡,姓名,性別。花木蘭也有這些屬性,但是很明顯二者的屬性完全不一樣。花弧有自己的非靜態成員方法‘騎馬殺敵’,同樣花木蘭也遺傳了父親一樣的方法‘騎馬殺敵’。花弧還有一個靜態方法‘自我介紹’,每個人都可以問花弧姓甚名誰。同時花木蘭還有一個自己特有的非靜態成員方法‘塗脂抹粉’。但是,現在花木蘭替父從軍,女扮男裝。這時候相當於父類的引用(花弧這個名字)指向了子類物件(花木蘭這個人),那麼在其他類(其他的人)中訪問子類物件(花木蘭這個人)的成員屬性(姓名,年齡,性別)時,其實看到的都是花木蘭她父親的名字(花弧)、年齡(60歲)、性別(男)。當訪問子類物件(花木蘭這個人)的非靜態成員方法(騎馬打仗)時,其實都是看到花木蘭自己運用十八般武藝在騎馬打仗。當訪問花木蘭的靜態方法時(自我介紹),花木蘭自己都是用她父親的名字資訊在向別人作自我介紹。並且這時候花木蘭不能使用自己特有的成員方法‘塗脂抹粉’。

-----多型中的向上轉型 那麼終於一將功成萬骨枯,打仗旗開得勝了,花木蘭告別了戰爭生活。有一天,遇到了自己心愛的男人,這時候愛情的力量將父類物件的引用(花弧這個名字)強制轉換為子類物件本來的引用(花木蘭這個名字),那麼花木蘭又從新成為了她自己,這時候她完全是她自己了。名字是花木蘭,年齡是28,性別是女,打仗依然那樣生猛女漢子,自我介紹則堂堂正正地告訴別人我叫花木蘭。OMG!終於,終於可以使用自己特有的成員方法‘塗脂抹粉’了。從此,花木蘭完全回到了替父從軍前的那個花木蘭了。並且和自己心愛的男人幸福的過完了一生。-----多型中的向下轉型

大家記得哈,向上轉型向下轉型一定是在多型這個前提下哈,否則強制將女兒變成父親,或者將父親變成女人,就變成東方不敗了,系統此時就會報錯非法型別轉換。哈哈哈哈哈。另外開發中一般是利用多型宣告形式引數,並將建立子類的匿名物件作為實際引數。以上。 

多型成員訪問特點

  • 成員變數 編譯看左邊(父類),執行看左邊(父類) 
  • 成員方法 編譯看左邊(父類),執行看右邊(子類)。動態繫結 
  • 靜態方法 編譯看左邊(父類),執行看左邊(父類)。
靜態和類相關,算不上重寫,所以,訪問還是左邊的,只有非靜態的成員方法,編譯看左邊,執行看右邊  

經典案例分析

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{

}

public class Test {
    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複製程式碼

分析這個題需要記住以下幾點

當父類物件引用變數引用子類物件時,被引用物件的型別決定了呼叫誰的成員方法,
引用變數型別決定可呼叫的方法。如果子類中沒有覆蓋該方法,那麼會去父類中尋找。複製程式碼

先看一個例子

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
複製程式碼

其實在繼承鏈中物件方法的呼叫存在一個優先順序:this.show(O)、super.show(O)、
this.show((super)O)、super.show((super)O)。複製程式碼

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的方法。 


abcd的關係是這樣的:C/D ---> B ---> A

  1.  a1是A類的一個例項化物件,所以this指向A,然後查詢this.show(b),由於沒有這個方法,所以到super.show(b),但是由於A類沒有父類了,所以到this.show(super b),由於b的父類是A,所以相當於this.show(A),然後在A類中查詢到了這個方法,於是輸出A and A。 
  2.  a1是A類的例項化物件,所以this指向A,然後在A類中查詢this.show(C)方法,由於沒有這個方法,所以到了super.show(C),由於A類的父類裡面找,但是A沒有父類,所以到了this.show(super C),由於C的父類是B所以在A類裡面查詢this.show(B)方法,也沒找到,然後B也有父類,就是A,所以查詢this.show(A),找到了,於是輸出A and A;
  3. a1是A類的例項化物件,所以this指向A,然後在A類中找到this.show(D)方法,找到了,所以就輸出A and D;
  4.  a2是B類的引用物件,型別為A,所以this指向A類,然後在A類裡面找this.show(B)方法,沒有找到,所以到了super.show(B),由於A類沒有父類,所以到了this.show(super B),B的父類是A,即super B = A,所以執行方法this。show(A),在A方法裡面找show(A),找到了,但是由於a2是一個類B的引用物件,而B類裡面覆蓋了A類的show(A)方法,所以最終執行的是B類裡面的show(A)方法,即輸出B and A; 
  5.  a2是B類的引用物件,型別為A,所以this指向A類,然後在A類裡面找this.show(C)方法,沒有找到,所以到了super.show(C)方法,由於A類沒有父類,所以到了this.show(super C),C的父類是B,所以在A類裡面找show(B),同樣沒有找到,發現B還有父類,即A,所以還繼續在A類裡面找show(A)方法,找到了,但是由於a2是一個類B的引用物件,而B類裡面覆蓋了A類的show(A)方法,所以最終執行的是B類裡面的show(A)方法,即輸出B and A;
  6. a2是B類的引用物件,型別為A,所以this指向A類,然後在A類裡面找this.show(D)方法,找到了,但是由於a2是一個類B的引用物件,所以在B類裡面查詢有沒有覆蓋show(D)方法,沒有,所以執行的是A類裡面的show(D)方法,即輸出A and D;
  7. b是B類的一個例項化物件,首相執行this.show(B),在B類裡面找show(B)方法,找到了,直接輸出B and B;
  8. b是B類的一個例項化物件,首相執行this.show(C),在B類裡面找show(C)方法,沒有找到,所以到了super.show(c),B的父類是A,所以在A類中找show(C)方法,沒有找到,於是到了this.show(super C),C的父類是B,所以在B類中找show(B)f方法,找到了,所以執行B類中的show(B)方法輸出B and B;
  9.  b是B類的一個例項化物件,首相執行this.show(D),在B類裡面找show(D)方法,沒有找到,於是到了super.show(D),B的父類是A類,所以在A類裡面找show(D)方法,找到了,輸出A and D;

總結

本篇文章的內容大體上就是這些了。我們來總結一下。

  1. 多型,簡而言之就是同一個行為具有多個不同表現形式或形態的能力。
  2. 多型的分類:執行時多型和編譯時多型。
  3. 執行時多型的前提:繼承(實現),重寫,向上轉型
  4. 向上轉型與向下轉型。
  5. 繼承鏈中物件方法的呼叫的優先順序:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。


相關文章