OODS · ส่วน OOP Inheritance และ Polymorphism

หัวข้อ 2 · 15 นาที

Polymorphism และการ cast

นึกภาพก่อน

บาเซนจิทุกตัวเป็นสุนัข และสุนัขทุกตัวเป็นสัตว์ แต่สัตว์บางตัวไม่ใช่สุนัข ถ้ามีกรงที่ติดป้ายว่า "สัตว์" จะใส่สุนัข แมว หรือบาเซนจิก็ได้ แต่กรงที่ติดป้ายว่า "สุนัข" ใส่แมวไม่ได้

ตัวแปรใน Java ก็เหมือนกรงที่มีป้าย ป้ายคือ ชนิดของตัวแปร ส่วนของที่อยู่ข้างในคือ อ็อบเจกต์จริง Polymorphism (หนึ่งอ็อบเจกต์แต่หลายบทบาท) คือการที่ตัวอ้างอิงของคลาสแม่อ้างอ็อบเจกต์ของคลาสลูกได้

ลำดับชั้นที่ใช้ตลอดบทนี้:

text
        Animal
        /    \
      Dog    Cat
       |
    Basenji

ตัวอ้างอิงของคลาสแม่อ้างอ็อบเจกต์ของลูกหลานได้

java
Dog d = new Dog();
Cat c = new Cat();
Basenji b = new Basenji();
Animal a = d;   // ได้
a = c;          // ได้
a = b;          // ได้ (Basenji เป็น Animal ผ่านทาง Dog)

b = d;          // แปลไม่ผ่าน: Dog ไม่จำเป็นต้องเป็น Basenji
d = c;          // แปลไม่ผ่าน: Cat ไม่ใช่ Dog

กฎ: กำหนดค่าได้เมื่อชนิดทางขวา เป็น (is-a) ชนิดทางซ้าย

Upcast และ Downcast

ตัวอย่างผล
upcast (ลูก → แม่)Animal a = new Dog();ทำได้เสมอ ไม่ต้องเขียน cast
ไม่ castDog d = a;แปลไม่ผ่าน
downcast (แม่ → ลูก)Dog d = (Dog) a;แปลผ่าน และทำงานได้ถ้าอ็อบเจกต์จริงเป็น Dog
downcast ผิดชนิดa = new Cat(); d = (Dog) a;แปลผ่าน แต่ตอนทำงานเกิด ClassCastException
cast ข้ามสายCat c = new Cat(); d = (Dog) c;แปลไม่ผ่าน

ทำไมสองแถวล่างต่างกัน:

  • (Dog) a เมื่อ a มีชนิด Animal: compiler รู้แค่ว่า a เป็น Animal ซึ่ง อาจจะ เป็น Dog ก็ได้ จึงยอมให้แปลผ่าน แล้วไปตรวจจริงตอนทำงาน
  • (Dog) c เมื่อ c มีชนิด Cat: compiler รู้แน่ว่า Cat ไม่มีทางเป็น Dog (คนละสาย) จึงปฏิเสธตั้งแต่ตอนแปล

เมท็อดไหนถูกเรียก

java
public class Dog extends Animal {
  public void bark() { System.out.println("โฮ่ง ๆ"); }
}
public class Basenji extends Dog {
  public void bark() { System.out.println("..."); }
}
java
Dog d = new Dog();
Basenji b = new Basenji();
d.bark();   // โฮ่ง ๆ
b.bark();   // ...
d = b;
d.bark();   // ...

บรรทัดสุดท้ายคือหัวใจของเรื่อง ตัวแปร d มีชนิด Dog แต่อ็อบเจกต์จริงเป็น Basenji ผลที่ได้คือของ Basenji

Java ตรวจสองรอบ:

  • ตอนแปล (compile-time) ดูคลาสของ ตัวแปร: d เป็นตัวแปรแบบ Dog และคลาส Dog มีเมท็อด bark จึงแปลผ่าน
  • ตอนทำงาน (run-time) ดูคลาสของ อ็อบเจกต์ที่ถูกอ้างอิง: d อ้างอ็อบเจกต์ของ Basenji จึงเรียก bark ของ Basenji

สองวิธีใช้งาน

แบบมีเมท็อดร่วมในคลาสแม่

ถ้า Animal มีเมท็อด bark() (แม้ไม่มีเนื้อความ) และคลาสลูก override กันเอง ก็วนเรียกได้เลย:

java
public class Animal {
  public void bark() { }
  public static void makeSound(Animal[] a) {
    for (int i = 0; i < a.length; i++) a[i].bark();
  }
}
java
Animal[] a = { new Dog(), new Cat(), new Basenji() };
Animal.makeSound(a);

ผลลัพธ์: โฮ่งๆ.. แล้ว เมี๊ยวๆ.. แล้ว ...... แต่ละอ็อบเจกต์ทำ bark ของตัวเอง

แบบไม่มีเมท็อดร่วม ต้องตรวจเองแล้ว downcast

ถ้า Animal ไม่มี bark() คำสั่ง a[i].bark() จะแปลไม่ผ่าน (ตอนแปลดูคลาสของตัวแปร) ต้องตรวจชนิดด้วย instanceof แล้ว cast:

java
for (Animal animal : a) {
  if (animal instanceof Dog) {
    ((Dog) animal).bark();
  } else if (animal instanceof Cat) {
    ((Cat) animal).bark();
  }
}

สรุปจากสไลด์:

  • ตัวชี้ของคลาสแม่ชี้ไปหาลูกได้ทุกตัว และเรียกเมท็อดได้โดยตรง ถ้าคลาสแม่มีเมท็อดนั้น
  • ถ้าคลาสแม่ไม่มี ต้อง downcast ก่อน เช่น ((Cat) a).Sound();

ตัวอย่างไล่ทีละขั้น

java
Animal a = new Animal();
Dog d = new Dog();
Cat c = new Cat();
Basenji b = new Basenji();
a = d;  a.Bark();
a = c;  a.Bark();
a = b;  a.Bark();
บรรทัดชนิดตัวแปร aอ็อบเจกต์จริงตอนแปลผลตอนทำงาน
a = d; a.Bark();AnimalDogAnimal มี Bark → ผ่านโฮ่งๆ...
a = c; a.Bark();AnimalCatผ่านเมี๊ยวๆ...
a = b; a.Bark();AnimalBasenjiผ่าน.....

จุดที่มักพลาด

1. ดูชนิดของตัวแปรเพื่อตัดสินว่าเมท็อดไหนทำงาน

ชนิดของตัวแปรใช้ตัดสินแค่ว่า แปลผ่านไหม ส่วนเมท็อดที่ทำงานจริงดูจากอ็อบเจกต์ Dog d = new Basenji(); d.bark(); ได้เสียงของ Basenji

2. คิดว่า downcast ที่แปลผ่านจะทำงานได้เสมอ

(Dog) a แปลผ่านเมื่อ a เป็นตัวแปร Animal แต่ถ้าอ็อบเจกต์จริงเป็น Cat จะเกิด ClassCastException ตอนทำงาน

3. แยกไม่ออกว่าผิดตอนแปลหรือตอนทำงาน

  • กำหนดค่าแม่ให้ตัวแปรลูกโดยไม่ cast → ผิดตอนแปล
  • cast ข้ามสาย (Cat เป็น Dog) → ผิดตอนแปล
  • downcast ในสายเดียวกันแต่อ็อบเจกต์จริงไม่ใช่ชนิดนั้น → ผิดตอนทำงาน

4. คิดว่า upcast ทำให้อ็อบเจกต์เปลี่ยนชนิด

อ็อบเจกต์ไม่เคยเปลี่ยน สิ่งที่เปลี่ยนคือ "ป้าย" ที่เรามองมัน Animal a = new Dog(); อ็อบเจกต์ยังเป็น Dog ครบทุกอย่าง

5. เรียกเมท็อดที่มีเฉพาะในลูกผ่านตัวแปรแม่

ถ้า Animal ไม่มี Sound() คำสั่ง a.Sound() แปลไม่ผ่าน แม้อ็อบเจกต์จริงจะเป็น Cat ที่มี Sound() ก็ตาม

ที่มา: Polymorhism.pdf หน้า 1–16