Java學習-多型

三千年一開花發表於2019-03-27

多型的基本理解

多型是同一個行為具有多個不同表現形式或形態的能力。是同一個介面,使用不同的例項而執行不同操作

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

上面的說法有些難以理解,舉個例子來說明一下:想象一下作為一個樂器收藏家的你,收集了很多的樂器,有Wind(管樂器),Stringed(絃樂)和Brass(管樂),你拿起任意一個樂器來都可以進行演奏,我們可以描述如下:

樂器 a=管樂器
樂器 b=絃樂
樂器 c=管樂

這裡所表現的就是多型。管樂器、玄月、管樂都是樂器的子類,我們只通過樂器這一父類就能夠引用不同的子類,這就是多型——我們只有在執行時才會知道引用變數所指向的具體例項物件。

同時在這裡我們也要先弄清楚什麼是“向上轉型”。在上面的例子中,樂器(Instrument)是父類,管樂器(Wind),絃樂(Stringed),管樂(Brass)是子類。我們定義如下程式碼:

Wind a=new Wind();

對於這個程式碼我們非常容易理解,就是例項化了一個管樂器的物件。但是下面這種呢?

Instrument a=new Wind();

在這裡,定義了一個Instrument型別的a,它指向了Wind物件例項。由於Wind是繼承於Instrument的,所以Wind可以自動向上轉型為Instrument,所以父類a的引用可以指向Wind例項物件的。這樣有一個很大的好處是在繼承中我們知道子類是父類的擴充套件,它可以提供比父類更加強大的功能,如果我們定義了一個指向子類的父類引用型別,那麼它除了能夠引用父類的共性外,還可以使用子類強大的功能。

但是向上轉型存在一些缺憾,那就是它必定會導致一些方法和屬性的丟失,而導致我們不能夠獲取它們。所以父類型別的引用可以呼叫父類中定義的所有屬性和方法,對於只存在與子類中的方法和屬性它就望塵莫及了

public class Instrument{
    public void play1(){
        System.out.println("Instrument 的play1.....");
        play2();
    }
    
    public void play2(){
        System.out.println("Instrument 的play2...");
    }
}

public class Wind extends Instrument{
    /**
     * @desc 子類過載父類方法
     *        父類中不存在該方法,向上轉型後,父類是不能引用該方法的
     * @param a
     * @return void
     */
    public void play1(String a){
        System.out.println("Wind 的 play1...");
        play2();
    }
    
    /**
     * 子類重寫父類方法
     * 指向子類的父類引用呼叫fun2時,必定是呼叫該方法
     */
    public void play2(){
        System.out.println("Wind 的play2...");
    }
}

public class Test {
    public static void main(String[] args) {
        Instrumenta = new Wind();
        a.play1();
    }
}
-------------------------------------------------
Output:
Instrument 的play1.....
Wind 的play2...

從程式的執行結果中我們發現,a.play1()首先是執行父類Instrument中的play1().然後再執行子類Wind中的play2()。

分析:在這個程式中子類Wind過載了父類Instrument的方法play1(),重寫play2(),而且過載後的play1(String a)與 play1()不是同一個方法,由於父類中沒有該方法,向上轉型後會丟失該方法,所以執行Wind的Instrument型別引用是不能引用play1(String a)方法。而子類Wind重寫了play2() ,那麼指向Wind的會引用會呼叫Wind中play2()方法。

所以對於多型我們可以總結如下:

指向子類的父類引用由於向上轉型了,它只能訪問父類中擁有的方法和屬性,而對於子類中存在而父類中不存在的方法,該引用是不能使用的,儘管是過載該方法。若子類重寫了父類中的某些方法,在呼叫該些方法的時候,必定是使用子類中定義的這些方法

對於物件導向而已,多型分為編譯時多型和執行時多型。其中編輯時多型是靜態的,主要是指方法的過載,它是根據引數列表的不同來區分不同的函式,通過編輯之後會變成兩個不同的函式,在執行時談不上多型。而執行時多型是動態的,它是通過動態繫結來實現的,也就是我們所說的多型性。

多型的實現

實現條件

在剛剛開始就提到了繼承在為多型的實現做了準備。子類Child繼承父類Father,我們可以編寫一個指向子類的父類型別引用,該引用既可以處理父類Father物件,也可以處理子類Child物件,當相同的訊息傳送給子類或者父類物件時,該物件就會根據自己所屬的引用而執行不同的行為,這就是多型。即多型性就是相同的訊息使得不同的類做出不同的響應。

Java實現多型有三個必要條件:繼承、重寫、向上轉型。

繼承:在多型中必須存在有繼承關係的子類和父類。

重寫:子類對父類中某些方法進行重新定義,在呼叫這些方法時就會呼叫子類的方法。

向上轉型:在多型中需要將子類的引用賦給父類物件,只有這樣該引用才能夠具備技能呼叫父類的方法和子類的方法。

只有滿足了上述三個條件,我們才能夠在同一個繼承結構中使用統一的邏輯實現程式碼處理不同的物件,從而達到執行不同的行為。

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

實現形式

Java中有兩種形式可以實現多型:繼承和介面。

基於繼承實現的多型

基於繼承的實現機制主要表現在父類和繼承該父類的一個或多個子類對某些方法的重寫,多個子類對同一方法的重寫可以表現出不同的行為。

public class Instrument {
    private String name;
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Instrument(){
    }
    
    public String play(){
        return "演奏的是 " + getName();
    }
    
    /**
     * 重寫toString()
     */
    public String toString(){
        return null;
    }
}

public class Wind extends Instrument{
    public Wind(){
        setName("Wind");
    }
    
    /**
     * 重寫父類方法,實現多型
     */
    public String play(){
        return "演奏的是 " + getName();
    }
    
    /**
     * 重寫toString()
     */
    public String toString(){
        return "Instrument : " + getName();
    }
}

public class Stringed extends Instrument{
    public Stringed(){
        setName("Stringed");
    }
    
    /**
     * 重寫父類方法,實現多型
     */
    public String play(){
        return "演奏的是 " + getName();
    }
    
    /**
     * 重寫toString()
     */
    public String toString(){
        return "Instrument : " + getName();
    }
}

public class Test {
    public static void main(String[] args) {
        //定義父類陣列
        Instrument[] instruments = new Instrument[2];
        //定義兩個子類
        Wind wind = new Wind();
        Stringed stringed = new Stringed();
        
        //父類引用子類物件
        Instrument[0] = wind;
        Instrument[1] = stringed;
        
        for(int i = 0 ; i < 2 ; i++){
            System.out.println(instruments[i].toString() + "--" + instruments[i].play());
        }
        System.out.println("-------------------------------");

    }
}
OUTPUT:
Instrument : Wind--演奏的是 Wind
Instrument : Stringed--演奏的是 Stringed
-------------------------------

在上面的程式碼中Wind、Stringed繼承Instrument,並且重寫了play()、toString()方法,程式執行結果是呼叫子類中方法,輸出Wind、Stringed的名稱,這就是多型的表現。不同的物件可以執行相同的行為,但是他們都需要通過自己的實現方式來執行,這就要得益於向上轉型了。

基於繼承實現的多型可以總結如下:對於引用子類的父類型別,在處理該引用時,它適用於繼承該父類的所有子類,子類物件的不同,對方法的實現也就不同,執行相同動作產生的行為也就不同。

如果父類是抽象類,那麼子類必須要實現父類中所有的抽象方法,這樣該父類所有的子類一定存在統一的對外介面,但其內部的具體實現可以各異。這樣我們就可以使用頂層類提供的統一介面來處理該層次的方法。

基於介面實現的多型

繼承是通過重寫父類的同一方法的幾個不同子類來體現的,那麼就可就是通過實現介面並覆蓋介面中同一方法的幾不同的類體現的。

在介面的多型中,指向介面的引用必須是指定這實現了該介面的一個類的例項程式,在執行時,根據物件引用的實際型別來執行對應的方法。

繼承都是單繼承,只能為一組相關的類提供一致的服務介面。但是介面可以是多繼承多實現,它能夠利用一組相關或者不相關的介面進行組合與擴充,能夠對外提供一致的服務介面。所以它相對於繼承來說有更好的靈活性。

相關文章