close

27   多型(Polymorphism)

多型(polymorphism)是物件導向程式語言的三個重要特徵之一,其他兩個是資料抽象化(data abstraction)及繼承(inheritance)

多型是指在繼承體系之下,某一物件能以其自身型別視之,亦可以其基礎型別(base type)視之。亦即能在同一繼承體系之下,將多個型別視為同一型別。所謂基礎型別即為父類別、父類別的父類別或是父類別的父類別的父類別、…等。這種將某個object reference視為一個reference to base type的動作,稱為向上轉型(upcasting),因為在繼承體系圖中,基礎型別總是畫在上方。

 圖27-1 類別繼承體系例

 

如上圖之類別繼承體系例,D1類別之物件可視為本身D1類別之型別,亦可視為父類別C1B1A之型別,B2類別可視為本身B2類別之型別,亦可視為父類別A之型別。上圖各類別可被轉換之型別如下表:

 

       27-1 可轉換型別

類別

可被轉換之型別

A

A

B1

B1A

C1

C1B1A

D1

D1C1B1A

B2

B2A

C2

C2B2A

 

 

子類別以父類別視之時,將會進行「窄化」,也就是只能使用父類別有宣告的子類別方法(method)-紅字method1,即使是子類別之方法有被覆寫,而無法使用子類別自身擴充之方法-method2,因為父類別型別沒有method2,除非子類別使用自身型別而不向上轉型。

 圖27-2 向上轉型窄化例

 

<例>

class GFather1{

  public void method1(){

    System.out.println("GFather1's method1");

  }

}

 

class Father1 extends GFather1{

  public void method1(){

    System.out.println("Father1's method1");

  }

}

 

class Son1 extends Father1{

  public void method1(){

    System.out.println("Son1's method1");

  }

  public void method2(){

    System.out.println("Son1's method2");

  }

}

 

public class Poly1{

  public static void main(String[] args){

    Son1 s = new Son1();

    Father1 f = new Father1();

    GFather1 g = new GFather1();

    g = s ; //Son1物件向上轉型至GFather1型別

    g.method1(); //執行的卻是Son1的方法

//因窄化關係,雖然是Son1物件卻無法執行method2

//  g.method2();    錯誤

    f = s ; //Son1物件向上轉型至Father1型別

    f.method1(); //執行的仍是Son1的方法

//因窄化關係,雖然是Son1物件卻無法執行method2

//  f.method2();    錯誤

    s.method2();   //只有本身型別物件可以執行method2

  }

}

C:\js>java Poly1

Son1's method1

Son1's method1

Son1's method2

 

 

[27-1 向上轉型的方式]

向上轉型的方式舉例如下,可依程式需求選擇之:

1.父類別及子類別分別建立物件,將子類別物件參考(即變數)代入父類別物件參考。

Father1 f = new Father1(); //分別建立父類別、子類別物件

Son1 s = new Son1();

f = s ; //向上轉型

f.method1(); //呼叫方法(子物件方法)

 

2.建立子物件時直接向上轉型

Father1 f = new Son1(); //建立子物件時直接向上轉型

f.method1(); //呼叫方法(子物件方法)

 

3.使用帶父類別屬性參數的方法

public void polyMethod(Gfather1 p){

    p.method1();

  }

 

方式1如前例Poly1,方式2舉例如下:

public class Poly2{

  public static void main(String[] args){

    GFather1 g = new Son1();

    g.method1();

    Father1 f = new Son1();

    f.method1();

//   s.method2();  因未建立Son1自身型別物件,故無法執行method2

  }

}

C:\js>java Poly2

Son1's method1

Son1's method1

 

方式3舉例如下:

public class Poly3{

  public static void main(String[] args){

    Poly3 p3 = new Poly3();

    Father1 f = new Father1();

    p3.poly3Method(f);

    Son1 s = new Son1();

    p3.poly3Method(s);

  }

//向上轉型用方法

  public void poly3Method(GFather1 p){

    p.method1();

  }

}

C:\js>java Poly3

Father1's method1

Son1's method1

 

public class Poly4{

  public static void main(String[] args){

    Poly4 p4 = new Poly4();

    p4.poly4Method(new Father1());

    p4.poly4Method(new Son1());

  }

//向上轉型用方法

  public void poly4Method(GFather1 p){

    p.method1();

  }

}

C:\js>java Poly4

Father1's method1

Son1's method1

 

 

[27-2 多型的運用]

現舉例說明多型的運用,為說明方便,此例已經過簡化。

某公司薪資計算繼承體系圖如下,其上有「正式月薪制員工類別(MEmployee)」,薪資為固定月薪,有應上班日而未上班須扣除部分薪水(deduction)。另有臨時時薪制員工類別(HEmployee)繼承自「正式月薪制員工類別」,月薪資為每小時工資(wageofhour)乘上當月工時(hrsofmon)。後來公司又招募臨時件薪制員工(PEmployee)」,繼承自臨時時薪制員工類別」,月薪資為每件工資(wageofpiece)乘上當月件數(piecesofmon)

 

 圖27-3 薪資計算繼承體系圖

[27-2-1 多型具程式簡潔性]

使用多型撰寫程式較非多型方式為簡潔。以下為招募臨時件薪制員工」前之程式碼,比較如下:

「正式月薪制員工類別(MEmployee)程式碼:

class MEmployee{

  int salofmon;      //月薪

  int deduction;     //月薪減項

  public int totSalMon(){

    return this.salofmon - this.deduction;

  }

}

 

「臨時時薪制員工類別(HEmployee)」程式碼:

class HEmployee extends MEmployee{

  int wageofhr;  //時薪

  int hrsofmon;  //當月工時

  public int totSalMon(){

    return this.wageofhr * this.hrsofmon;

  }

}

 

以下各「員工薪資計算(CalcuSalary1~4)」程式執行時,在「命令提示字元」命令列需輸入剛好4個引數,兩個引數中間要有空白:

引數1-輸入部門代碼(數字1)

正式月薪制員工輸入"1"、臨時時薪制員工輸入"2"、臨時件薪制員工輸入"3"

引數2-輸入員工編碼(數字5)

引數3-輸入單位薪資(數字)

正式月薪制員工輸入"月薪"、臨時時薪制員工輸入"時薪"、臨時件薪制員工輸入"件薪"

引數4-輸入減項、工時、件數(數字)

正式月薪制員工輸入"月薪減項"、臨時時薪制員工輸入"當月工時"、臨時件薪制員工輸入"當月件數"

 

[非多型例]

「員工薪資計算(CalcuSalary1)」程式碼:

public class CalcuSalary1{

  String empno;

  public static void main(String[ ] args){

    if (args.length < 4){

      System.out.println("請輸入4個命令列引數。") ;

      System.exit(1);

    }

    CalcuSalary1 c = new CalcuSalary1();

    c.empno = args[1];

    switch (args[0]) {

      case "1":

        MEmployee m = new MEmployee();

        m.salofmon = Integer.parseInt(args[2]);

        m.deduction = Integer.parseInt(args[3]);

        System.out.println("員工" + c.empno + "本月薪資" + m.totSalMon() + "");

        break;

      case "2":

        HEmployee h = new HEmployee();

        h.wageofhr = Integer.parseInt(args[2]);

        h.hrsofmon = Integer.parseInt(args[3]);

        System.out.println("員工" + c.empno + "本月薪資" + h.totSalMon() + "");

        break;

      default:

        System.out.println("員工代碼錯誤");

        System.exit(1);

    }

  }

}

C:\js>java CalcuSalary1 5 55555 29000 1300

員工代碼錯誤

 

C:\js>java CalcuSalary1 1 11112 35000 0

員工11112本月薪資35000

 

C:\js>java CalcuSalary1 2 22221 98 250

員工22221本月薪資24500

 

[多型例]

「員工薪資計算(CalcuSalary2)」程式碼:

public class CalcuSalary2{

  String empno;

public static void main(String[ ] args){

    if (args.length < 4){

        System.out.println("請輸入4個命令列引數。") ;

        System.exit(1);

    }

    CalcuSalary2 c = new CalcuSalary2();

    c.empno = args[1];

   switch (args[0]) {

      case "1":

        MEmployee m = new MEmployee();

        m.salofmon = Integer.parseInt(args[2]);

        m.deduction = Integer.parseInt(args[3]);

        c.cal(m);

        break;

      case "2":

        HEmployee h = new HEmployee();

        h.wageofhr = Integer.parseInt(args[2]);

        h.hrsofmon = Integer.parseInt(args[3]);

        c.cal(h);

        break;

      default:

        System.out.println("員工代碼錯誤");

        System.exit(1);

    }

  }

//因向上轉型使用下述同一方法(method)

  public void cal(MEmployee p){

    System.out.println("員工" + empno + "本月薪資" + p.totSalMon() + "");

  }

}

C:\js>java CalcuSalary2

請輸入4個命令列引數。

 

C:\js>java CalcuSalary2 1 11111 30000 4990

員工11111本月薪資25010

 

C:\js>java CalcuSalary2 2 22222 100 240

員工22222本月薪資24000

 

 

[27-2-2 多型具擴充彈性]

該公司後因業務需求招募臨時件薪制員工(PEmployee)」,該類別繼承自「時薪制員工類別(HEmployee)」,月薪資為每件工資(wageofpiece)乘上當月件數(piecesofmon),且有績效點數(10件為1)以鼓勵效率高的件薪制員工,於年底發放績效獎金。

「臨時件薪制員工類別(PEmployee)」程式碼:

class PEmployee extends HEmployee{

  int wageofpiece;  //件薪

  int piecesofmon;  //當月件數

  int point;      //績效點數

public int totSalMon(){

    return wageofpiece * piecesofmon;

  }

  public int ptMon(){

    return piecesofmon / 10;  //績效點數計算

  }

}

 

[非多型例]

「員工薪資計算(CalcuSalary3)」程式碼:

public class CalcuSalary3{

  String empno;

  public static void main(String[ ] args){

    if (args.length < 4){

        System.out.println("請輸入4個命令列引數。") ;

        System.exit(1);

    }

    CalcuSalary3 c = new CalcuSalary3();

    c.empno = args[1];

   switch (args[0]) {

      case "1":

        MEmployee m = new MEmployee();

        m.salofmon = Integer.parseInt(args[2]);

        m.deduction = Integer.parseInt(args[3]);

       System.out.println("員工" + c.empno + "本月薪資" + m.totSalMon() + "");

        break;

      case "2":

        HEmployee h = new HEmployee();

        h.wageofhr = Integer.parseInt(args[2]);

        h.hrsofmon = Integer.parseInt(args[3]);

        System.out.println("員工" + c.empno + "本月薪資" + h.totSalMon() + "");

        break;

     //增加之程式碼

     case "3":

        PEmployee p = new PEmployee();

        p.wageofpiece = Integer.parseInt(args[2]);

        p.piecesofmon = Integer.parseInt(args[3]);

        System.out.println("員工" + c.empno + "本月薪資" + p.totSalMon() + "");

        break;

      default:

          System.out.println("員工代碼錯誤");

          System.exit(1);

    }

  }

}

C:\js>java CalcuSalary3 3 33337 25 1210

員工33337本月薪資30250

 

[多型例]

「員工薪資計算(CalcuSalary4)」程式碼:

public class CalcuSalary4{

  String empno;

  public static void main(String[ ] args){

    if (args.length < 4){

        System.out.println("請輸入4個命令列引數。") ;

        System.exit(1);

    }

    CalcuSalary4 c = new CalcuSalary4();

    c.empno = args[1];

   switch (args[0]) {

      case "1":

          MEmployee m = new MEmployee();

          m.salofmon = Integer.parseInt(args[2]);

          m.deduction = Integer.parseInt(args[3]);

          c.cal(m);

          break;

      case "2":

          HEmployee h = new HEmployee();

          h.wageofhr = Integer.parseInt(args[2]);

          h.hrsofmon = Integer.parseInt(args[3]);

          c.cal(h);

          break;

     //增加之程式碼

     case "3":

          PEmployee p = new PEmployee();

          p.wageofpiece = Integer.parseInt(args[2]);

          p.piecesofmon = Integer.parseInt(args[3]);

          c.cal(p);

          break;

      default:

          System.out.println("員工代碼錯誤");

          System.exit(1);

      }

  }

   //因向上轉型使用下述同一方法(method)

  public void cal(MEmployee p){

    System.out.println("員工" + empno + "本月薪資" + p.totSalMon() + "");

  }

}

C:\js>java CalcuSalary4 3 33335 25 1200

員工33335本月薪資30000

 

 

[27-3 多型的侷限性]

如前述,向上轉型會窄化介面(即方法)之使用,轉型之後的物件只能呼叫基礎型別有定義之方法,如薪資計算(totSalMon)。而子類別(臨時件薪制員工類別)所擴充之方法-績效點數計算(ptMon)則無法呼叫,須以自身型別的物件來呼叫。下述例子僅作為說明窄化會有編譯錯誤之情形發生。

<>

public class CalcuPoint1{

  public static void main(String[ ] args){

    MEmployee m = new HEmployee();

    m.totSalMon();

    m.ptMon();   //因窄化無法呼叫

  }

}

C:\js>javac CalcuPoint1.java

CalcuPoint1.java:5: error: cannot find symbol

    m.ptMon();   //因窄化無法呼叫

     ^

  symbol:   method ptMon()

  location: variable m of type MEmployee

1 error

 

<>

public class CalcuPoint2{

  public static void main(String[ ] args){

    MEmployee m = new PEmployee();

    m.totSalMon();

    PEmployee p = new PEmployee();

    p.ptMon();  //自身型別可呼叫

  }

}

C:\js>javac CalcuPoint2.java

編譯正常。

 

 

[27-4 繫結(binding)]

繫結(binding)是建立方法呼叫(method call)和方法本體(method body)之間的關聯,即方法執行時要執行哪一個方法,如同名時要執行父類別還是子類別方法。繫結(binding)可分兩種,一種是程序性語言所採用的,稱為先期繫結(early binding),繫結動作發生於程式執行前,由編譯器(compiler)和連結器完成(linker)。物件導向程式語言Java的所有方法(method),除了宣告為staticfinal(private methods自然而然成為final)外,皆採用後期繫結(late binding),繫結動作在執行期才根據物件型別來進行,亦稱為執行期繫結(run-time binding)或動態繫結(dynamic binding)

 

 

[27-5 多型其他舉例]

以列印顯示宋朝三蘇詩詞為例。

[27-5-1 以非多型方式執行例]

[27-5-1-1]各物件自行列印顯示

[1]蘇洵

class PolFat {

  public void poem00(){

  String[] 蘇洵 = new String[5];

 蘇洵[0] = "宋 蘇洵 初發嘉州";

 蘇洵[1] = "家託舟航千里速,";

 蘇洵[2] = "心期京國十年還。";

 蘇洵[3] = "烏牛山下水如箭,";

 蘇洵[4] = "忽失峨眉枕席間。";

for(String poem:蘇洵) //使用foreach列印顯示陣列內容

     System.out.println(poem);

  }

}

public class PolPoem00 {

  public static void main(String[] args) {

    PolFat fat = new PolFat();

    fat.poem00();

  }

}

C:\js>java PolPoem00

蘇洵初發嘉州

家託舟航千里速,

心期京國十年還。

烏牛山下水如箭,

忽失峨眉枕席間。

 

[2]蘇軾

class PolSon1 extends PolFat {

  public void poem00(){

  String[] 蘇軾 = new String[5];

 蘇軾[0] = "宋 蘇軾 鷓鴣天";

 蘇軾[1] = "林斷山明竹隱牆,亂蟬衰草小池塘。";

 蘇軾[2] = "翻空白鳥時時見,照水紅蕖細細香。";

 蘇軾[3] = "村舍外,古城旁,杖藜徐步轉斜陽。";

 蘇軾[4] = "殷勤昨夜三更雨,又得浮生一日涼。";

for(String poem:蘇軾) //使用foreach列印陣列內容

     System.out.println(poem);

  }

}

public class PolPoem01 {

  public static void main(String[] args) {

    PolSon1 son1 = new PolSon1();

    son1.poem00();

  }

}

C:\js>java PolPoem01

蘇軾鷓鴣天

林斷山明竹隱牆,亂蟬衰草小池塘。

翻空白鳥時時見,照水紅蕖細細香。

村舍外,古城旁,杖藜徐步轉斜陽。

殷勤昨夜三更雨,又得浮生一日涼。

 

[3]蘇轍

class PolSon2 extends PolFat {

  public void poem00(){

  String[] 蘇轍= new String[5];

 蘇轍[0] = "蘇轍七夕";

 蘇轍[1] = "火流知節換,秋到喜身安。";

 蘇轍[2] = "林鵲真安往,河橋晚未完。";

 蘇轍[3] = "得閒心不厭,求巧老應難。";

 蘇轍[4] = "送酒誰知我,瓢樽昨暮乾。";

  for(String poem:蘇轍) //使用foreach列印陣列內容

     System.out.println(poem);

  }

}

public class PolPoem02 {

  public static void main(String[] args) {

    PolSon2 son2 = new PolSon2();

    son2.poem00();

  }

}

C:\js>java PolPoem02

蘇轍七夕

火流知節換,秋到喜身安。

林鵲真安往,河橋晚未完。

得閒心不厭,求巧老應難。

送酒誰知我,瓢樽昨暮乾。

 

[27-5-1-2]統合各物件並選擇列印顯示

如非以多型方式撰寫時,main方法類別亦可合併如下述,即PolPoem00 PolPoem01PolPoem02合併。

[4]

public class NPolPoem {

  public static void main(String[] args) {

   if (args.length != 1) {

      System.out.println

("請輸入要顯示列印詩詞作者宋朝三蘇(蘇洵、蘇軾、蘇轍)之一的姓名");

     System.exit(1);

   }

   String writer = args[0];

   switch (writer) {

       case "蘇洵":

             PolFat fat = new PolFat();

              fat.poem00();

              break;

        case "蘇軾":

             PolSon1 son1 = new PolSon1();

              son1.poem00();

              break; 

       case "蘇轍":

             PolSon2 son2 = new PolSon2();

              son2.poem00();

              break;

       default:

              System.out.println("輸入了其他值");

              System.exit(1);

    }

  }

}

C:\js>java NPolPoem 蘇軾

宋 蘇軾 鷓鴣天

林斷山明竹隱牆,亂蟬衰草小池塘。

翻空白鳥時時見,照水紅蕖細細香。

村舍外,古城旁,杖藜徐步轉斜陽。

殷勤昨夜三更雨,又得浮生一日涼。

 

[27-5-2 多型例]

[27-5-2-1]各物件自行列印顯示

123main方法類別改寫如下:

[5]不需向上轉型

public class PolPoem00a {

  public static void main(String[] args) {

    PolFat fat = new PolFat();

    fat.poem00();

  }

}

C:\js>java PolPoem00a

蘇洵初發嘉州

家託舟航千里速,

心期京國十年還。

烏牛山下水如箭,

忽失峨眉枕席間。

 

[6]

public class PolPoem01a {

  public static void main(String[] args) {

    PolFat fat = new PolSon1();

    fat.poem00();

  }

}

執行結果:C:\js>java PolPoem01a

蘇軾鷓鴣天

林斷山明竹隱牆,亂蟬衰草小池塘。

翻空白鳥時時見,照水紅蕖細細香。

村舍外,古城旁,杖藜徐步轉斜陽。

殷勤昨夜三更雨,又得浮生一日涼。

 

[7]

public class PolPoem02a {

  public static void main(String[] args) {

    PolFat fat = new PolSon2();

    fat.poem00();

  }

}

C:\js>java PolPoem02a

蘇轍七夕

火流知節換,秋到喜身安。

林鵲真安往,河橋晚未完。

得閒心不厭,求巧老應難。

送酒誰知我,瓢樽昨暮乾。

 

[27-5-2-2]統合各物件並選擇列印顯示

[8]

public class PolPoem {

  public static void main(String[] args) {

    if (args.length != 1) {

      System.out.println

("請輸入要顯示列印詩詞作者宋朝三蘇(蘇洵、蘇軾、蘇轍)之一的姓名");

      System.exit(1);

   }

   String writer = args[0];

   switch (writer) {

        case "蘇洵":

              PolFat fat = new PolFat();

              poemprint(fat);

              break;

        case "蘇軾":

              PolSon1 son1 = new PolSon1();

              poemprint(son1);

              break; 

        case "蘇轍":

              PolSon2 son2 = new PolSon2();

              poemprint(son2);

              break;

        default:

              System.out.println("輸入了其他值");

              System.exit(1);

    }

  }

public static void poemprint(PolFat wr) {

    wr.poem00();

  }

}

C:\js>java PolPoem

請輸入要列印顯示詩詞作者宋朝三蘇(蘇洵、蘇軾、蘇轍)之一的姓名

 

C:\js>java PolPoem 蘇軾

蘇軾鷓鴣天

林斷山明竹隱牆,亂蟬衰草小池塘。

翻空白鳥時時見,照水紅蕖細細香。

村舍外,古城旁,杖藜徐步轉斜陽。

殷勤昨夜三更雨,又得浮生一日涼。

 

C:\js>java PolPoem 蘇轍

蘇轍七夕

火流知節換,秋到喜身安。

林鵲真安往,河橋晚未完。

得閒心不厭,求巧老應難。

送酒誰知我,瓢樽昨暮乾。

 

C:\js>java PolPoem 酥酥

輸入了其他值

 

C:\js>java PolPoem 蘇洵

蘇洵初發嘉州

家託舟航千里速,

心期京國十年還。

烏牛山下水如箭,

忽失峨眉枕席間。

 

 

[27-6 說明]

本章節所舉例子和實際專案開發比較或許不盡適當,但足供多型知識自學之用。且因所舉例子為便於快速了解而較為簡略,但當專案之類別繼承達較大規模時,便可看出多型功能對專案開發的簡潔靈活。

arrow
arrow

    祈泊 發表在 痞客邦 留言(0) 人氣()