2016/5/3

OOP:多型(Polymorphism) 以Java為例

參考2暨南教科書多型基本觀念
參考3什麼是物件導向(3):Polymorphism
參考4Java 快速導覽 - 物件導向概念 多型
參考5劉逸-論物件導向part 5:Polymorphism
參考6程式設計俱樂部-OOP 中對 "多型" 的理解
參考7歐萊禮-多型

這裡主要是討論OOP多型的概念,爬了些資料自己做些整理。
首先看到參考1良葛格所寫,它把多型想成「is-a 關係」(是一種關係)來看,讓初學者很容易初步的理解多型的概念,與什麼時候的多型寫法會編譯失敗



一般來說這你一定看得懂1
SwordsMan swordsMan = new SwordsMan();

那這呢?例2
Role role1 = new SwordsMan();
Role role2 = new Magician();
SwordsMan和Magician都繼承了Role,所以它兩都是一種Role
OK所以這兩行可編譯可執行

再來例3
SwordsMan swordsMan = new Role();
這會失敗,因為Role不一定是一種swordsMan

再看例4
Role role2 = new Magician();
SwordsMan swordsMan = (SwordsMan) role2;
我們先來看第一行,我自己的解釋,宣告一個Role型態的變數role2,也是參考名稱,role2參考Magician的instance。
第二行,宣告一個SwordsMan型態的變數swordsMan,並且把Role型態的role2轉型成SwordsMan型態。
這個(SwordsMan)寫法叫型態轉換,分UpCasting(向上轉型)和DownCasting(向下轉型),子類別subclass轉父類別superclass是UpCasting,反之就是DownCasting。
所以這個例子Role是父、SwordsMan是子,把role2轉SwordsMan就是DownCasting。
而通常DownCasting會比較不容易,所以這兩行會編譯成功但執行失敗。
白話講,role2要扮演SwordsMan型態,所以它扮演成功,系統誤認,編譯成功但執行失敗。
你可能想問,為何執行失敗?因為Role不一定是一種swordsMan!

多型可以幹嘛?好處?
來看參考1良葛格中的例5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package cc.openhome;
public class RPG {
    public static void main(String[] args) {
        SwordsMan swordsMan = new SwordsMan();
        swordsMan.setName("Justin");
        swordsMan.setLevel(1);
        swordsMan.setBlood(200);

        Magician magician = new Magician();
        magician.setName("Monica");
        magician.setLevel(1);
        magician.setBlood(100);
        
        showBlood(swordsMan);
        showBlood(magician);
    }
    static void showBlood(Role role) {
        System.out.printf("%s 血量 %d%n",
                role.getName(), role.getBlood());
    }
}
這樣子,以後我不管有幾個職業,只要用同個method,就可以做同樣的事。

再來看參考3所寫的解釋:
一個訊息(message or event or stimulus)的意義是由接收者(接收到這個訊息的物件)來解釋,而不是由訊息發出者(sender)來解釋。所以,在runtime時只要接受者換成不同的物件或是instance,系統的行為就會改變。具有這樣的特性就稱之為polymorphism。
 例5完全可以印證這個解釋!!

參考3中也有一個例子也來看看例6
List<String> list = new LinkedList<String>();
list.add(“This is a book”);
寫程式的人送了一個add這個訊息給list這個instance,如果list這個instance不是像上面程式一樣寫死new LinkedList<String>();而是runtime時傳入的任意符合List介面的某個instance,那麼程式的行為就無法只從靜態的程式碼得知了,而且add(“This is a book”);訊息的解釋也只能看runtime時list instance指到哪一個instance而定。
看到這,多少對多型有一點概念了吧,我們繼續看下去~
參考5中作者直接定義了多型:
就定義面而言多型大多定義如下:將相同的訊息傳遞給不同的物件,進而引發出不同的行為反應。而就程式面的實做部分,我常將多型定義如下:
  • 利用父類別的型態
  • 接受子類別的物件
  • 做相同的動作
  • 引發不同的行為
 來看他舉的例子例7
1
2
3
4
5
public void show(Animal a) {
   a.walk();
   a.barking();
   a.jump();
}
以後只要:
Tiger t = new Tiger;
Show(t); à這樣就會變成老虎走路、發出叫聲、最後跳火圈囉
 注意此method和例5的method 參數,都是一個父類別型態的參數,他也寫道多型起手式:
『Inheritance』 +『Overriding』+『Upcasting』。多型伴隨著一定是有父子類別的關係,或介面跟實做的關係,藉由overriding子類別會將父類別的方法重新定義來符合自身所需;另一方面,我們會針對上層的抽象類別或介面來撰寫邏輯上的運作,避免這些邏輯運作跟實體的子類別產生關聯,如此對日後的維護將有大大的幫助,以上述的馬戲團為例:show()這個函式都是針對上層提供的方法來操作,但於執行期間系統會自動依據他所接收的Animal a此一物件所屬的類別來動態繫結show當中各邏輯運作所應該對應的方法為何。
 開始來多看幾個例子例8參考1中的例子:
SwordsMan swordsMan2 = new SwordsMan();
Role role = swordsMan2; // SwordsMan是一種Role,這行通過編譯
// 你告訴編譯器要讓Role扮演SwordsMan,以下這行通過編譯
SwordsMan swordsMan = (SwordsMan) role; // role參考SwordsMan實例,執行成功
 第三行(SwordsMan) role ,其實就是DownCasting。

例9,詳細原始碼請看參考4
1
2
3
4
Animal puppy1, puppy2, puppy3; 
puppy1 = new Elephant("大象", 6, 70);
puppy2 = new Elephant();
puppy3 = new Elephant("林旺", 88, 5000);
puppy1 、 puppy2 與 puppy3 被宣告為 Animal 型態,但其後以 Elephant 型態的建構子建立物件,這個意思是說,對父類別, 也就是 Animal 型態的參考變數,實際可以由子類別,也就是 Elephant 型態來建立物件。

如想要專業一點的解釋,應該就是這樣了吧,例4只是我不負責任解釋QQ

參考2例10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package polymorphismanimal;class Animal {
    public String moveMethod() {
        return "Unspecified";
    }
}class Bird extends Animal {
    public String moveMethod() {
        return "Fly";
    }
}class Dog extends Animal {
    public String moveMethod() {
        return "run";
    }
}class Fish extends Animal {
    public String moveMethod() {
        return "swim";
    }
}public class PolymorphismAnimal {
    public static void main(String[] args) {
        Animal a1 = new Animal();
        System.out.println(a1.moveMethod());
        Fish a2 = new Fish();
        System.out.println(a2.moveMethod());
        Animal a3 = new Bird();
        System.out.println(a3.moveMethod());
    }
}
Unspecifiedrun:
swim
Fly

再引用一段參考6中我覺得不錯,又有點專業的說法:
多型是物件導向機制中一個很重要的《物件結構化》觀念。
一般在OOA、OOD的過程中,會發展出一些功能或範疇相近的類別,它們有類似的行為與功能,但卻不適合塞近同一個類別中。這時為了規範開發原則,常會利用一個介面或基礎類別來延伸這些子類別,以強制這些子類別有完全一樣的屬性與方法,達到規格最佳化的目的。

參考7中寫道:
「當某變數的實際型態(actual type)和形式型態(formal type)不一致時,呼叫此變數的 method,一定會呼叫到「正確」的版本, 也就是實際型態的版本。
 並且在多型中注意兩點:
  • 多型的機制只用在 method 上,不用在 field 上。
  • 多型的機制只用在 instance method上,不用在 class method 上。
這裡開始來說明一下什麼是實際型態、形式型態 
int a = 3;
long b =a;
我們先不管這兩行程式碼背後記憶體怎麼運作的,詳細請去看參考7多型的部份,我們先初步知道,左邊的int a和long b 叫形式型態(宣告時的形態),右邊叫實際型態(值的型態)。
對b來說它的實際和形式型態都一樣,因為它不是一種reference 型態。

例11
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package oreilly_classc;
class ClassA {
    void method1() {
        System.out.println("method1() in ClassA");
    }
    void method2() {
        System.out.println("method2() in ClassA");
    }
}
class ClassB extends ClassA {
  void method1() {
    System.out.println("method1() in ClassB");
  }
  void method3() {
    System.out.println("method3() in ClassB");
  }
}
public class Oreilly_ClassC {
    public static void main(String[] args) {
        ClassA a = new ClassB();
        a.method1();   // method1() in ClassB
        a.method2();  // method2() in ClassA
        a.method3();  // compile-time error
    }
}
來看21行,它會呼叫哪個method1?答:會叫ClassB的,因為20行兩邊型態不一致,這時就看實際型態了,所以它會叫ClassB()這實際型態的method1。
看22行,它會叫ClassA的method2,這裡我覺得是因為ClassB繼承ClassA了,所以ClassB也擁有method2,所以當然就可以呼叫。
23行,編譯錯誤,因為a是ClassA的形式型態,而ClassA沒有method3,雖然ClassB有,但編譯時只看形式型態,而不管實際型態,所以這時就會編譯錯誤!
形式型態也稱編譯期型態(compile-time type)
實際型態執行期型態(runtime type)

例12,配合例11看:
1
2
3
4
5
6
7
8
9
10
11
class ClassD {
  static void method4(ClassA a) {
  a.method1();   
  }
}
public classE {
  public static void main(String[] args) {
    ClassB b = new ClassB();
    ClassD.method4(b);
  }
}
這裡我們可以說,ClassD 的 method4() 需要 ClassA 的介面,不管是 ClassA 的 instance 和 ClassB 的 instance 都能符合這樣的要求(ClassB 繼承了 ClassA 的介面), 所以都能當作 method4() 的參數,和 ClassD.method4() 合作無間。任何對象只要能符合 ClassA 的介面,就能和 ClassD 合作無間,ClassD 的程式碼不需要進行任何修改。 多型的機制,讓程式具備了動態的擴充性。

作者整理出多型的發生必須符合:
  • 有直接或間接繼承關係的兩個類別,子類別的 method 將父類別的 method 予以override。
  • 實際型態為子類別的物件,被當成父類別來使用,呼叫其 overrided method。
再來,多型的機制只用在 method 上,不用在 field 上 好處:
  • 繼承程式碼:達到程式碼再用性(reuse)
  • 繼承介面(interface)。達到介面再用性(reuse),為多型預作準備
例13
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ClassA {
  String field1 = 'A1';
  String field2 = 'A2';
}

class ClassB extends ClassA {
  String field1 = 'B1';
  String field3 = 'B3';
  }

public class ClassC {
  public static void main(String[] args) {
    ClassA a = new ClassB();
    System.out.println(a.field1());  // "A1"
    System.out.println(a.field2());  // "A2"
    System.out.println(a.field3());  // compile-time error
  }
}
所以 field 主要看形式型態。

沒有留言:

張貼留言