2009年7月1日 星期三

物件繼承的物件觀點-轉型篇

這篇文章是物件繼承的物件觀點一文的續集,將用物件的功能觀點圖來說明物件的強制轉型何時會成功。
首先,我們先釐清一些有關於物件轉型的概念。
在Java中,所有子類別的物件不需要強制轉型就能夠隱含轉換為父類別型態。因此以下程式碼皆能夠通過編譯,也能正常執行。

女人 欣郁 = new 女人();
人類 路人甲 = 欣郁;

人類 阿呆 = new 男人();





至少,根據LSP,子類別型態的物件理應能夠被當作父類別型態的物件使用。子類別物件能夠被父類別型態的指標儲存位置,是再正常不過。至於這種做法合理的原因,在上篇文章也已經用功能觀點圖解釋過。
※LSP原則,簡而言之就是「所有子類別都可以代父出征。」

然而,如果在使用強制轉型的情況下,我們如何知道物件能夠被成功轉型,而不會造成執行時期的ClassCastException?
例如,以下程式碼:

人類 路人甲 = new 女人();
女人 欣郁 = (女人)路人甲;
人類 路人乙 = new 男人();

女人 如花 = (女人)路人乙;


在把路人甲從「人類」型態強制轉型成「女人」型態時並不會出錯,但如果把路人乙強制轉型成「女人」型態,就會在程式執行時拋出ClassCastException例外。直接從字面意義上來看,路人甲本質上是「女人」,從「人類」型態再度轉型成「女人」型態,沒什麼不對;路人乙本質上是「男人」,要把他變成「女人」,當然是不合理的。但這裡可以很容易看出不合理,是因為類別很符合人類思維,萬一類別間的關係並不是那麼直觀,要用什麼方法才比較容易看出?
其實用物件的功能觀點圖就可以簡單判斷能否合法轉型!
例如:

人類 路人甲 = new 女人();
女人 欣郁 = (女人)路人甲;

這兩行程式碼可以用下圖描述:




「人類」型態的路人甲指標把「女人」物件窄化,因此可以使用強制轉型轉回「女人」物件,並使用欣郁指標儲存。
然而以下程式碼就不行:

人類 路人乙 = new 男人();
女人 如花 = (女人)路人乙;




上圖根本就沒有「女人」物件可以給如花指標儲存,所以執行期間會拋出ClassCastException就很正常了。

那如果是以下程式碼,又要如何用圖表示?

人類 路人甲 = new 人類();
男人 如花 = (男人)路人甲;




同樣的道理,路人甲指標指向的物件並沒有「男人」類別的實體,因此會轉型失敗。
因此可以歸納出一個結論:物件能不能被轉型成功(無論是自動或是強制),取決於物件中是否有目標型態的實體。
那麼,要怎樣才能從用程式碼判斷某一個物件中是否有某個類別的實體? 在Java中可以用instanceof指令來判斷。

在此,先寫出一個女人.生產()的實作:
class 女人{
....
   public 人類 生產(){
       int rand = (int)(Math.random()*2);
       return rand == 0? new 女人(): new 男人();
   }
}

這段程式碼代表的是,女人.生產()的回傳值有一半的機率是「男人」的實體,一半的機率是「女人」的實體。無論是「男人」或是「女人」,都會被窄化為「人類」型態並回傳。這很直觀,因為女人不可能只會生出某一種性別的胎兒,因此將女人.生產()的回傳值型態設為人類是有必要的。然而,不會有人想用「動物」型態當女人.生產()的回傳值型態,除非是預謀上演貍貓換太子的戲碼。
接著,我們用程式碼來描述「欣郁生了孩子」。
女人 欣郁 = new 女人();
人類 欣郁的孩子 = 欣郁.生產();

我們可以用以下程式碼,來得知欣郁的孩子究竟是男是女:
if( 欣郁的孩子 instanceof 男人)
    System.out.println("恭喜,是個男孩。");
else if(欣郁的孩子 instanceof 女人)
    System.out.println("恭喜,是個女孩。");
else
    System.out.println("糟糕,性別不詳");

※我的一個朋友和我高中時期的一位教官同名同性,名字都叫「欣郁」。這時你就得思考,究竟生孩子的是哪個「欣郁」。從比例來看,男性教官的比例較女性教官高,因此後者為男性的可能性較高。然而事實上,我的友人「欣郁」卻是個男性,而我的教官「欣郁」確確實實是個為人母的女性。我在這裡使用「欣郁」當變數名稱,是想表達變數涵義的混淆不清,用「如花」也是基於同樣的理由。

沒有留言:

張貼留言