1. 程式人生 > >Java之物件的多型性(使用生活中通俗的例子講解)

Java之物件的多型性(使用生活中通俗的例子講解)

Java之物件的多型性

多型概念 (Java)

    多型英語:polymorphism),是指計算機程式執行時相同的訊息可能會送給多個不同的類別之物件,而系統可依據物件所屬類別,引發對應類別的方法,而有不同的行為。簡單來說,所謂多型意指相同的訊息給予不同的物件會引發不同的動作稱之

多型也可定義為“一種將不同的特殊行為和單個泛化記號相關聯的能力”。

多型可分為變數多型與函式多型。變數多型是指:基型別的變數(對於C++是引用或指標)可以被賦值基型別物件,也可以被賦值派生型別的物件。函式多型是指,相同的函式呼叫介面(函式名與實參表),傳送給一個物件變數,可以有不同的行為,這視該物件變數所指向的物件型別而定。

因此,變數多型是函式多型的基礎。

例子

    比如有動物(Animal)之類別(Class),而且由動物繼承出類別雞(Chicken)和類別狗(Dog),並對同一源自類別動物(父類別)之一訊息有不同、的響應,如類別動物有“叫()”之動作,而類別雞會“啼叫()”,類別狗則會“吠叫()”,則稱之為多型。

概括

    上面關於多型的概念看起來有那麼一點難以理解,但是我們可以把上述一大段話給歸納成為一句話就是:相同的訊息可能會送給多個不同的類別之物件,而系統可依據物件所屬類別,引發對應類別的方法,而有不同的行為

例如:比如有動物(Animal)之類別(Class),而且由動物繼承出類別雞(Chicken)和類別狗(Dog),並對

同一源自類別動物(父類別)之一訊息有不同的響應。

解釋

方法的覆蓋特性是Java在執行時支援的多型性之一。系統動態的排程方法是由呼叫一個被覆蓋的方法引起的,該呼叫機制發生在執行時,而不是編譯時。

  • 當一個被覆蓋的方法通過一個父類引用呼叫時,Java決定執行哪個版本的方法(父類的方法或者被子類覆蓋的方法)取決於方法呼叫發生時,被引用的物件的型別。因此,這一決定實在執行期間做出來的。
  • 在執行期間,哪個版本的被覆蓋的方法將會被執行是由被引用的物件的型別決定的而不是引用的型別(值)。
  • 一個父類的引用可以引用一個子類的物件,這也是一些人口中所說的“向上轉型”,Java用這個方法來解決程式執行時覆蓋方法被呼叫的問題。

    因此,如果一個父類包含了一個被子類覆蓋的方法,接下來通過一個父類的引用值來引用其不同的子類物件,父類引用呼叫被覆蓋的方法時,不同版本的被覆蓋的方法將會執行。下面是一個例子,簡單的闡述一下這種動態方法分配機制:



OUTPUT(輸出):


程式詳解

    上面的這個程式建立了一個名為A的父類以及兩個分別名為B,C的子類。這兩個子類都覆蓋了父類A類的m1方法

1.在main()方法裡面的Dispatch類中,首先聲明瞭A類,B類,C類的三個物件。


    此時的引用及物件之間的關係如下圖所示:


    其中,A類引用指向了一個A類物件,B類引用指向了一個B類物件,B類物件內包含了一個從A類中繼承下來的m1()方法以及自己覆蓋m1的方法;C類引用指向了一個C類物件,C類物件內也包含了一個從A類中繼承下來的m1()方法以及自己覆蓋m1的方法;

2.現在宣告一個名為ref的A類的引用,起初它將會指向一個空指標。


3.接下來我們將用A類引用來一個一個地指向每一種物件(A類B類或者C類),並且通過這個A類引用ref來呼叫m1()方法。正如輸出所顯示的那樣,到底執行哪一個版本的m1()方法是由此時引用指向的物件的型別所決定的。







親自動手驗證

           有興趣的同學可以去這個線上IDE平臺自己試一下這串程式碼,親自動手驗證一下上面這個例子。


執行時成員變數的多型性


    在Java中,我們只能夠覆蓋父類的方法,卻不能覆蓋父類的成員變數。

所以執行時,成員變數無法實現多型性。舉一個例子:


    在上面這個例子中,子類B類雖然覆蓋了父類A類的成員變數X,然而通過A類引用去呼叫B類物件的X時返回的並不是B類中已經覆蓋了A類的那個X的值,返回的仍然是父類A類中的X值,也就是說,成員變數覆蓋無法實現像方法覆蓋那樣的多型性。雖然A類(父類)和B類(子類)都有一個公共的變數x,我們例項化一個B類物件並用一個A類引用a指向它。因為變數沒有被覆蓋,所以“a.x”這一句話總是代表父類的資料成員。

OUTPUT(輸出):

親自試一試

    有興趣的同學可以點選此處通過線上IDE平臺親自實驗一下,更好的理解這個例子。

靜態VS動態繫結

    1.靜態繫結發生在程式的編譯期間,而動態繫結發生在程式的執行期間。

    2.private,final,static方法以及變數通過編譯器實現靜態繫結,同時覆蓋方法與執行時執行的物件型別所繫結。

對於多型的通俗理解

        其實,對於引用的理解,我們可以簡單的理解其為一個“遙控器”,每個“遙控器”可以與一個物件進行“配對”,並通過遙控器來控制物件的“行為”。例如,按下空調遙控器的開關鍵,空調就會開啟。就好比呼叫 空調遙控器.開機()方法使得空調遙控器指向的空調物件執行開機()方法一樣。


多型的誤區點


  其中,物件能夠執行哪些方法是由引用的型別所決定的,而物件具體怎樣去執行某個方法,卻是由物件自己決定的。

例如下面這段程式:

class A {
    public void fun1() {
        System.out.println("A-->public void fun1(){}");
    }

    public void fun2() {
        this.fun1();
    }
}

class B extends A {
    public void fun1() {
        System.out.println("B-->public void fun1(){}");
    }

    public void fun3() {
        System.out.println("B-->public void fun3(){}");
    }
}

public class JavaExample {
    public static void main(String[] args) {
        A a = new B();
        a.fun1();
    }
}

執行結果為:B-->public void fun1(){};

 上面的程式中,定義了父類為A類,子類為B類,子類B覆蓋了父類的fun1()方法,添加了自己的fun3()類,這是一個物件向上轉型的關係,但是A型別的引用a雖然可以呼叫fun1()方法,但是不能通過A類引用a呼叫子類物件獨有的fun3()方法!!!

因此,我們可以得出下面這個結論:

引用所能夠呼叫的方法取決於引用的型別,而如何具體的實現該方法取決於物件的型別。

引用a為A型別,所以通過引用a只能呼叫A型別中包含的方法( fun1()和fun2()),而能呼叫B類中的fun3(。也就是說A類引用只能向指向的物件傳送A類中包含的方法,具體的實現由指向的物件的型別來決定。

                    點選此處進入線上IDE實驗本案例。

我們可以在上面的實驗中,執行a.fun3(),將會提示找不到fun3()方法。

向下轉型的要求


    還是上面的這一串程式碼,我們只更改其main()函式中的程式碼:

public class JavaExample {
    public static void main(String[] args) {
       A a=new B();
       B b=(B)a;
       b.fun1();
       b.fun2();
       b.fun3();
    }
}

    B類為A類的子類,上面的a首先被宣告為一個A型別的引用,並指向一個B型別的物件,接下來把A類引用通過向下轉型變成一個B類引用,從而有權利去呼叫B類中非共有的方法。

    然而,在看下面這一段程式碼,還是隻更改main()函式中的程式碼:

public class JavaExample {
    public static void main(String[] args) {
       A a=new A();
       B b=(B)a;
       b.fun1();
       b.fun2();
       b.fun3();
    }

    上面的這一段程式碼也實現了向下轉型,然而執行時卻報錯了:

Exception in thread "main" java.lang.ClassCastException: A cannot be cast to B

at JavaExample.main(JavaExample.java:26)

錯誤提示說A類引用無法被轉換成B類引用,這又是怎麼回事呢?

我們發現兩端程式碼的不同之處就在於第一段程式碼中是

 A a=new B();

而第二段程式碼中是:

  A a=new A();

    初始化的物件不同而已,因為在物件進行向下轉型時,必須首先發生物件向上轉型,否則將出現物件轉換異常。

第二段程式碼中,A型別引用指向的是一個A類物件,把A類引用轉換成B類引用,此時就可以傳送呼叫B類方法的命令給指向的物件,而指向的物件為A類物件卻並不含B類中非共有的方法,物件根本就沒有實現這個方法,怎麼去執行方法呢?因此就會產生錯誤。

可以用上述遙控器的例子來簡單的理解一下這個問題:

      假設最開始只有一個老式遙控器,遙控器只有1.開啟空調,2.關閉空調,3.製冷三個功能,遙控器向空調發送相應的命令,空調收到命令後執行相應的動作。

    此時類似於執行語句 A a=new A( );


    接下來,老式空調太舊了,空調廠商在老式空調的基礎上,開發了一款新式空調,不僅有老式空調的三個功能,而且還加入了1.制熱功能和2.加溼功能這兩個功能。

此時類似於語句 class B extend A;


    然而,我們此時只有老式空調的遙控器,但是我們還是可以用老式空調的遙控器與操作新式空調,類似於A a=new B( )這個操作,因為新式空調是繼承與老式空調改造而來的,我們仍然能夠使用老式空調的遙控去操控新式空調,只不過老式空調上只有最基礎的三個功能,不能操縱新式空調執行其新新增的加熱和加溼功能,但是仍然還是可以使用的。


        如果,我們需要使用新式空調的新增的加熱和加溼功能,那麼我們理應更換新式空調對應的遙控器就好了。

此時類似於 B b=(B) a;

    

此時的情況就類似於呼叫b.makeCold( )方法了。

   上面是正常的向下轉型,然而,如果我們不向上轉型而直接向下轉型會發生什麼樣的情況呢?

例如:A a=new A( );

                B b=(B)a;

            就發生了下面這種情況:


    此時,如果新式空調遙控器向老式空調發送一個加熱的指令,然而老式空調收到指令後卻發現並沒有加熱的功能,此時就會發生系統故障。

        所以,總結而之一句話:在物件進行向下轉型時,必須首先發生物件向上轉型,否則將出現物件轉換異常。

參考資料

    1.維基百科-多型

    2.https://www.geeksforgeeks.org/dynamic-method-dispatch-runtime-polymorphism-java

        作者:Gaurav Miglani   翻譯:劉揚俊

    3.《Java開發實戰經典》 李興華著  清華大學出版社

部落格文章版權說明


第一條本部落格文章僅代表作者本人的觀點,不保證文章等內容的有效性。

第二條本部落格部分內容轉載於合作站點或摘錄於部分書籍,但都會註明作/譯者和原出處。如有不妥之處,敬請指出。

第三條徵得本部落格作者同意的情況下,本部落格的作品允許非盈利性引用,並請註明出處:“作者:____轉載自____”字樣,以尊重作者的勞動成果。版權歸原作/譯者所有。未經允許,嚴禁轉載

第四條 對非法轉載者,“揚俊的小屋”和作/譯者保留採用法律手段追究的權利

第五條本部落格之宣告以及其修改權、更新權及最終解釋權均屬“揚俊的小屋”。

第六條 以上宣告的解釋權歸揚俊的小屋所有。