หัวข้อ 2 · 15 นาที
Polymorphism และการ cast
นึกภาพก่อน
บาเซนจิทุกตัวเป็นสุนัข และสุนัขทุกตัวเป็นสัตว์ แต่สัตว์บางตัวไม่ใช่สุนัข ถ้ามีกรงที่ติดป้ายว่า "สัตว์" จะใส่สุนัข แมว หรือบาเซนจิก็ได้ แต่กรงที่ติดป้ายว่า "สุนัข" ใส่แมวไม่ได้
ตัวแปรใน Java ก็เหมือนกรงที่มีป้าย ป้ายคือ ชนิดของตัวแปร ส่วนของที่อยู่ข้างในคือ อ็อบเจกต์จริง Polymorphism (หนึ่งอ็อบเจกต์แต่หลายบทบาท) คือการที่ตัวอ้างอิงของคลาสแม่อ้างอ็อบเจกต์ของคลาสลูกได้
ลำดับชั้นที่ใช้ตลอดบทนี้:
Animal
/ \
Dog Cat
|
Basenji
ตัวอ้างอิงของคลาสแม่อ้างอ็อบเจกต์ของลูกหลานได้
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 |
| ไม่ cast | Dog 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 (คนละสาย) จึงปฏิเสธตั้งแต่ตอนแปล
เมท็อดไหนถูกเรียก
public class Dog extends Animal {
public void bark() { System.out.println("โฮ่ง ๆ"); }
}
public class Basenji extends Dog {
public void bark() { System.out.println("..."); }
}
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 กันเอง ก็วนเรียกได้เลย:
public class Animal {
public void bark() { }
public static void makeSound(Animal[] a) {
for (int i = 0; i < a.length; i++) a[i].bark();
}
}
Animal[] a = { new Dog(), new Cat(), new Basenji() };
Animal.makeSound(a);
ผลลัพธ์: โฮ่งๆ.. แล้ว เมี๊ยวๆ.. แล้ว ...... แต่ละอ็อบเจกต์ทำ bark ของตัวเอง
แบบไม่มีเมท็อดร่วม ต้องตรวจเองแล้ว downcast
ถ้า Animal ไม่มี bark() คำสั่ง a[i].bark() จะแปลไม่ผ่าน (ตอนแปลดูคลาสของตัวแปร) ต้องตรวจชนิดด้วย instanceof แล้ว cast:
for (Animal animal : a) {
if (animal instanceof Dog) {
((Dog) animal).bark();
} else if (animal instanceof Cat) {
((Cat) animal).bark();
}
}
สรุปจากสไลด์:
- ตัวชี้ของคลาสแม่ชี้ไปหาลูกได้ทุกตัว และเรียกเมท็อดได้โดยตรง ถ้าคลาสแม่มีเมท็อดนั้น
- ถ้าคลาสแม่ไม่มี ต้อง downcast ก่อน เช่น
((Cat) a).Sound();
ตัวอย่างไล่ทีละขั้น
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(); | Animal | Dog | Animal มี Bark → ผ่าน | โฮ่งๆ... |
a = c; a.Bark(); | Animal | Cat | ผ่าน | เมี๊ยวๆ... |
a = b; a.Bark(); | Animal | Basenji | ผ่าน | ..... |
จุดที่มักพลาด
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