27 多型(Polymorphism)
多型(polymorphism)是物件導向程式語言的三個重要特徵之一,其他兩個是資料抽象化(data abstraction)及繼承(inheritance)。
多型是指在繼承體系之下,某一物件能以其自身型別視之,亦可以其基礎型別(base type)視之。亦即能在同一繼承體系之下,將多個型別視為同一型別。所謂基礎型別即為父類別、父類別的父類別或是父類別的父類別的父類別、…等。這種將某個object reference視為一個reference to base type的動作,稱為向上轉型(upcasting),因為在繼承體系圖中,基礎型別總是畫在上方。
如上圖之類別繼承體系例,D1類別之物件可視為本身D1類別之型別,亦可視為父類別C1、B1及A之型別,B2類別可視為本身B2類別之型別,亦可視為父類別A之型別。上圖各類別可被轉換之型別如下表:
表27-1 可轉換型別
類別 |
可被轉換之型別 |
A |
A |
B1 |
B1、A |
C1 |
C1、B1、A |
D1 |
D1、C1、B1、A |
B2 |
B2、A |
C2 |
C2、B2、A |
子類別以父類別視之時,將會進行「窄化」,也就是只能使用父類別有宣告的子類別方法(method)-紅字method1,即使是子類別之方法有被覆寫,而無法使用子類別自身擴充之方法-method2,因為父類別型別沒有method2,除非子類別使用自身型別而不向上轉型。
<例>
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-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),除了宣告為static或final(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 PolPoem01、PolPoem02合併。
[例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]各物件自行列印顯示
例1、2、3之main方法類別改寫如下:
[例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 說明]
本章節所舉例子和實際專案開發比較或許不盡適當,但足供多型知識自學之用。且因所舉例子為便於快速了解而較為簡略,但當專案之類別繼承達較大規模時,便可看出多型功能對專案開發的簡潔靈活。