多型

多型主要的目的是為了讓程式的撰寫上更具有彈性與擴充性,而所謂的多型即意謂著單一的物件可以被宣告成多種型別。在Java語言中多型常見於繼承結構與介面(interface)之中,例如現有一個A類別繼承至B類別,在建立A物件時可以有兩種方式:

A objectA = new A();

B objectB = new A();

由於A類別是繼承至B類別,所以我們可以將A物件轉型成為B物件型態,若讀著還記得先前章節所介紹的轉型方式,該還記得由小資料型別轉大資料型別並不會造成資料流失因此可以自動轉型,但由大資料型別轉小資料型別可就得非得利用強制轉不可,同樣的,若我們將上式改寫成:

A objectaA=new B();

將發生編譯錯誤的情況,原因是B類別只是被A類別繼承,所以B 類別並無法得知A類別內部所提供的資料與方法,聰明的讀者這時一定想到若利用強制轉型方式便可以順利的轉型過去,但這樣做將會發生執行時期錯誤(Run time error),這將會讓你的程式隱藏著不可預期的錯誤(因為是由B物件並不認得A物件的內容)。

【多型範例程式】

//多型範例

【執行結果】

2-21  多型範例執行結果

圖 17 多型範例執行結果

或許讀者直接閱讀程式碼會覺得很抽像,下例四張圖分別代表範例中的baseObject、baseToSuperObject、superObject與runtimeErrObject物件,透過將抽像的物件轉換成具體的圖形可以讓我們更加的容易了解物件之間內部的運作情形。

  • baeObject物件

在範例中我們利用關鍵字new將BaseClass類別實體化為物件後,再指派給同樣為BaseClass物件型別的baseObject變數,因此由圖中可以發現baseObject與最初實體化BaseClass物件,所指到的是同一個物件型別,接著由於BaseClass繼承自SuperClass類別,因此JavaVM會先將父類別SuperCalss實體化為物件,接著再將子類別BaseClass實體化,會這麼做的原因是為了確保父類別物件存在且可使用,如此子類別才能夠順利的呼叫叫父類別的函式。Java對具有繼承結構的物件方法呼叫有一套處理規則,當我們對物件呼叫某一個函式時,若該物件擁有繼承的架構,則JavaVM將會從最底層的子類別開始尋找函式,若找不到再向父類別呼叫尋找,一但在傳遞途中找到了該函式,就不在往上層呼叫,並執行該函式,若一直往上傳遞到最上層的父類都還找不到該函式,則將傳回找不到函式的錯誤訊息。

利用這套規則baseObject物件首先呼叫了superMethod1()函式,並且在BaseClass物件中找到了該函式名稱,雖然在父類別SuperClass物件中也有提供相同名稱的函式,但因為所有的函式呼叫都是從最底層子類別開始尋找,所以父類別的同名函式將永遠不會被執行到,這種情形在物件導向中稱為覆寫(overriding),至於為何要有覆寫的功能,簡單的說利用覆寫可以不必設定一堆方法的名稱,只需利用一個方法名稱就能依當時物件所屬的類別層級做適當的回應,如此可以讓程式更加的精簡。同樣的superMethod1與baseMethod的函式呼叫都是依據這套規則,分別會在SuperClass與BaseClass物件中被呼叫執行。

2-22 baseClass物件運作圖

圖 18 baseObject物件運作圖

  • baseToSuperObject物件

在這裡baseToSuperObject物件可以充份的表現出Java多型的特性,由下圖可以看到,首先我們將BaseClass物件實體化後指派至SuperClass型別的baseToSuperObject變數,雖然baseToSuperObject變數本身是指向SuperCalss型別,但本身骨子裡卻是由BaseClass子類別所實體化出來的,因此當baseToSuperObject.Method1()被呼叫時儘管目前的變數型別是指向SuperClass物件,但由於baseToSuperObject骨子裡還是由BaseClass物件實作而成,所以依然必須導守,繼承結構函式呼叫規則,函式呼叫會從最初實體人的型態BaseClass物件開始尋找,若找不到再往上層父類別SuperClass呼叫。直得注意的是baseToSuperObject.baseMethod(); 這行程式碼將會發生編譯時期的錯誤,因為baseToSuperObject變數目前是指向SuperClass型別,而在SuperClass物件中並找不到baseMethod()這個函式的存在。解決這類問題的方法有二種,第一種為在父類別SuperClass撰寫一個空的baseMethod函式,如此一樣編譯時期便可以順利的在SuperClass物件中找到baseMethod()方法,並由於該方法被子類別BaseClass所覆寫,所以在執行時期會先於BaseCalss中找到該方法並執行。第二種解決方法為直接將baseTosuperObjet變數強制轉型回BaseClass型別,如此一來在就不會有找不到baseMethod函式的問題存在。所以在多型的世界裡,物件可以有多種不同的型別,但骨子裡卻還是保有最初被實體化時的型別,藉由多型的觀念,物件可以依不同的型別層級做出不同的回應,讓程式的設計更加的精簡靈活。

2-23 baseToSuper物件運作圖

圖 19 baseToSuperObject物件運作圖

  • superClass物件

上述二個範例物件都是以BaseClass為基礎,接著讓我們改以父類別SuperCalss物件為基礎,並呼叫同樣的三個函式,看看會有什麼不同的執行結果。在下圖中superObject參數所指向的物件型別為SuperClass正好與new關建字後所實體化的體物型態相同,因此 superObject並不會認得BasseClass物件,父類別也不會對子類別進行件實體化的動作,(簡單的說在繼承結構裡,只有下層(子類別)會認得上層(父類別)的物件,而上層(父類別)物件並不會知道有誰繼承了它)所以所有的函式呼叫都將會直接跳到SuperClass物件裡去尋找,並不在具有繼承結構時的函式呼叫規則,因此當baseMethod函式被呼叫時,將會發生編譯時期的錯誤,原因在於SuperClass物件中並沒有定義這個函式,此時就算是利用強制轉型的方式,將superObject轉型為BaseClass型別,雖然在編譯時基是合法的,但在執行時期時依然會錯誤,原因在於superObject最初是由SuperClass類別實體化而來,所以並不會認得BaseClass的內容,所以光是強制轉型過去依然是無法執行的。

2-25 superClass物件運作圖

圖 110 superObject物件運作圖

  • rutimeErr物件

在下圖為將SuperClass類別實體化為物件後在轉型為BaseClass型態,並指派至runtimeErrObjetct變數中,由於父類別無法自動轉型為子類別,因此在範例中是利用強制轉型的方式,雖然這樣做在編譯時期看來一切都為合法,並不會有什麼錯誤發生,但在執行時期時,由於SuperClass物件並不會認得,以下程式碼是一個錯誤的示範,因此在範例程式碼中是被註解起來的,讀者可以試著把註解拿掉編譯看看,編譯器並不會允許以下的語法結構。由於runtimeErrObject是由SuperClass類別實體化而成的物件,因此子類別BaseClass物件將不會被自動的實體化,且SuperClass也不會知有那些類別繼承了自己,因此這類的轉型在執行時期是不被允許的。

2-24 runtimeErr物件運作圖

圖 111 runtimeErObjectr物件運作圖