โหมดมืด
บทที่ 6.2 — Polymorphism & Abstract class
📚 บทที่ 6 (OOP ขั้นต่อ) แบ่งเป็น 3 ตอน — ตอนนี้คือ 6.2: 6.1 Inheritance · 6.2 Polymorphism & Abstract · 6.3 Interface & Composition
ต่อจาก 6.1 (inheritance) — ตอนนี้เอา inheritance มาใช้จริงผ่าน polymorphism และ class แม่แบบ abstract
1. Polymorphism (พหุสัณฐาน) — "หลายร่าง"
📖 Polymorphism = ความสามารถที่ object เดิมทำงานต่างกันได้ตาม type จริงตอน runtime (ตอนที่โปรแกรมกำลังทำงานจริง ๆ — ต่างจาก compile-time ที่ตรวจก่อนรัน) (พหุสัณฐาน = "หลายร่าง")
หัวใจของ OOP — object เดียวมองได้หลายมุม
📖 ปูพื้นก่อน — 3 แนวคิดสำคัญ (อ่านทีละข้อ ก่อนดูตัวอย่างหลัก):
1. Java เป็น static typing — ทุกตัวแปรต้องประกาศ type ตอน compile เช่น
Animal aต้องบอกไว้ตั้งแต่แรกว่าaเป็น typeAnimal(ต่างจาก Python/JS ที่ตัวแปรไม่ต้องประกาศ type)2. compile-time type vs runtime type — Java แยกสอง type ของตัวแปรเดียวกัน:
- compile-time type = type ที่ประกาศไว้ตอนเขียนโค้ด (compiler เห็นแค่นี้)
- runtime type = type ของ object จริงที่ตัวแปรถืออยู่ตอนโปรแกรมทำงาน (อาจเป็น "ลูก" ของ compile-time type ก็ได้)
ตัวอย่าง:
javaAnimal a = new Dog(); // compile-time type = Animal (compiler รู้แค่นี้) // runtime type = Dog (object จริงคือ Dog)3. Upcasting — การประกาศตัวแปร parent (เช่น
Animal) ให้ถือ object ของ child (เช่นDog) ทำได้อัตโนมัติโดยไม่ต้อง cast เพราะ Dog "เป็น" Animal อยู่แล้ว (ตาม inheritance ที่เรียนไปในบทที่แล้ว)
ก่อนเริ่ม — สมมติบทที่ 6.1 เราเขียน class เหล่านี้ไว้:
📝 หมายเหตุ: class Animal ในบทนี้ใช้
privatefield พร้อม getter/setter ตาม encapsulation ที่เรียนในบทที่ 5 — ต่างจากตัวอย่างง่าย ๆ ใน 6.1 ที่ใช้ field โดยตรง ถ้านำโค้ดจากทั้งสองบทมารวมกันให้ปรับ field ให้ตรงกันก่อน
java
class Animal {
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public void makeSound() { System.out.println("..."); } // เสียงทั่วไป
}
class Dog extends Animal {
@Override
public void makeSound() { System.out.println("Woof!"); }
public void bark() { System.out.println("Woof woof!"); } // method เฉพาะของ Dog
}
class Cat extends Animal {
@Override
public void makeSound() { System.out.println("Meow!"); }
}
class Bird extends Animal {
@Override
public void makeSound() { System.out.println("Tweet!"); }
}ตอนนี้ดูบรรทัด magic นี้:
java
Animal a = new Dog(); // ⬅ upcasting — ตัวแปร Animal ถือ Dog ได้เพราะ Dog "เป็น" Animal
a.makeSound(); // "Woof!" — Java เลือก method ของ Dog ตอน runtimeทำไมเขียนแบบนี้ได้: Dog "เป็น" Animal — ตัวแปรประเภท Animal ถือ Dog ได้
1.1 ทำไมต้อง polymorphism
มันเปิดทาง "เขียนโค้ดที่ทำงานกับ type หลายแบบ":
java
public static void describeAnimal(Animal a) {
System.out.println("Name: " + a.getName());
a.makeSound();
}
describeAnimal(new Dog()); // Woof!
describeAnimal(new Cat()); // Meow!
describeAnimal(new Bird()); // Tweet!method เดียวรองรับ Animal ทุกชนิด — ถ้าเพิ่ม Fish ใหม่ method นี้ใช้ได้ทันทีโดยไม่ต้องแก้
1.2 Array / List ของ Animal
java
Animal[] zoo = {
new Dog(),
new Cat(),
new Bird()
};
for (Animal a : zoo) {
a.makeSound(); // Java เลือก method ที่ถูกต้องเอง (dynamic dispatch — กระบวนการเลือก method ตอนรัน)
}— Java runtime เลือก method ตาม "type จริง" ของ object — ไม่ใช่ type ของตัวแปร
1.3 Dynamic dispatch — กลไกข้างใน
java
Animal a = new Dog();
a.makeSound();ตอน compile: Java เช็คว่า Animal มี makeSound → OK ตอน run: Java ดูว่า a ชี้ที่ Dog → เรียก Dog's makeSound
พฤติกรรมแบบนี้เรียกว่า runtime polymorphism (พหุสัณฐานตอนรัน) — method ที่ถูกเรียกจริงขึ้นอยู่กับ object จริง ไม่ใช่ type ที่ประกาศไว้ กลไกเบื้องหลังเรียกว่า dynamic dispatch (การส่งงานแบบเลือกตอนรัน) ผู้ที่ตัดสินใจว่าจะเรียก method ตัวไหนคือ JVM (Java Virtual Machine — ตัวรัน Java) และ JVM ตัดสินใจตอน run ไม่ใช่ตอน compile
⚠️ Pitfall — อย่าเรียก overridable method จาก constructor: ถ้า parent constructor เรียก method ที่ child override ไว้ Java จะเรียก method ของ child ก่อนที่ child fields จะถูก initialize — ทำให้ child fields ยังเป็นค่า default (0 หรือ null) เกิด bug ที่หาสาเหตุยาก ตัวอย่างจริง:
javaclass Animal { Animal() { makeSound(); // ❌ เรียก overridable method จาก constructor } void makeSound() { System.out.println("..."); } } class Dog extends Animal { private String breed = "Labrador"; // ยังไม่ทันถูกกำหนดค่าตอน Animal() ทำงาน @Override void makeSound() { System.out.println("Breed: " + breed); // พิมพ์ "Breed: null" ไม่ใช่ "Breed: Labrador" } } // new Dog() → เรียก Animal() ก่อน → Animal() เรียก makeSound() ที่ override เป็นของ Dog // แต่ตอนนั้น breed ของ Dog ยังไม่ถูก assign (เพราะ Dog's field initializer รันหลัง super() เสร็จ) // ผลลัพธ์: "Breed: null" ทั้งที่ตั้งใจให้เป็น "Breed: Labrador"
1.4 Cast ลง subtype
java
Animal a = new Dog();
a.bark(); // ❌ error: Animal ไม่มี bark
// Cast ลง Dog
Dog d = (Dog) a;
d.bark(); // ✅
// หรือ pattern matching (Java 16+)
if (a instanceof Dog d) { // เช็ค + cast ในคำเดียว
d.bark(); // ภายใน if block ตัวแปร d มีอยู่ scope แค่ตรงนี้
}
// นอก if block จะเรียก d ไม่ได้💡 Pattern matching syntax (Java 16+):
instanceof Dog d— ถ้าใช่จะ castaให้กลายเป็นตัวแปรใหม่d(type = Dog) ใช้ได้เฉพาะใน block ที่เงื่อนไขเป็นจริง ก่อน Java 16 ต้องเขียนDog d = (Dog) a;แยกบรรทัด
⚠️ Java version note: syntax
instanceof Dog dใช้ได้ตั้งแต่ Java 16+ (stable, ไม่ต้อง preview flag) — ใน Java 25 ที่เราติดตั้งใช้ได้เลย ถ้าใช้ Java 11 จะได้ compile error "patterns in instanceof are not supported in -source 11" ให้ใช้Dog d = (Dog) a;แทน
⚠️ ถ้า cast ผิด → ClassCastException (ข้อผิดพลาดขณะรันเนื่องจาก cast type ไม่ถูกต้อง):
java
Animal a = new Cat();
Dog d = (Dog) a; // ❌ runtime errorใช้ instanceof ก่อนเสมอ
2. Abstract class — class ที่สร้าง object ไม่ได้
บางทีเราอยากมี class แม่ที่ "ห้ามสร้างตรง ๆ" — เพราะมันไม่สมเหตุสมผล
เช่น "Animal" — มันไม่ใช่สัตว์ตัวจริง มันเป็นแนวคิด — ต้องเป็น Dog/Cat ของจริงเท่านั้น
java
abstract class Animal {
String name;
// abstract method — ไม่มี body, ลูกต้อง implement
abstract void makeSound();
// normal method — ลูกได้สืบทอดเลย
void sleep() {
System.out.println(name + " is sleeping");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof!");
}
}กฎ:
abstract class— สร้าง object ตรง ๆ ไม่ได้:new Animal()❌abstract method— ไม่มี body, child ต้อง implement ทุกอัน- ถ้า child ไม่ implement หมด → child ต้องเป็น abstract ด้วย
java
Animal a1 = new Animal(); // ❌ error: Animal is abstract
Animal a2 = new Dog(); // ✅ (Dog ไม่ abstract)3. ตัวอย่างใหญ่ — ระบบรูปทรง
โจทย์: คำนวณพื้นที่ของรูปทรงต่าง ๆ (วงกลม, สี่เหลี่ยม, สามเหลี่ยม)
java
abstract class Shape {
public abstract double area();
public void describe() {
System.out.println(getClass().getSimpleName() + " area: " + area()); // getSimpleName() = ชื่อ class สั้น ๆ เช่น "Circle"
}
}
class Circle extends Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
class Rectangle extends Shape {
private final double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
class Triangle extends Shape {
private final double base, height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double area() {
return 0.5 * base * height;
}
}
// ใช้งาน
public class Main {
public static void main(String[] args) {
Shape[] shapes = {
new Circle(5),
new Rectangle(4, 6),
new Triangle(3, 8)
};
double totalArea = 0;
for (Shape s : shapes) {
s.describe();
totalArea += s.area();
}
System.out.println("Total: " + totalArea);
}
}นี่คือ "polymorphism จริง" — เพิ่ม shape ใหม่ (เช่น Pentagon) ไม่ต้องแก้ method ที่ใช้
📝 หมายเหตุ — ไฟล์เดียว หลาย class: โค้ดบล็อกด้านบนมีหลาย class อยู่ด้วยกัน (Shape, Circle, Rectangle, Triangle, Main) — Java อนุญาตให้มีหลาย class ในไฟล์เดียวได้ แต่มีข้อห้ามสำคัญ: ไฟล์หนึ่งมี
public classได้แค่ตัวเดียว และชื่อต้องตรงกับชื่อไฟล์ ถ้าต้องการแยกเป็นหลายไฟล์ให้สร้างCircle.java,Rectangle.javaฯลฯ ต่างหาก
4. Checkpoint
🛠️ Checkpoint 6.2 — Employee inheritance
สร้างระบบ:
- abstract class
Employee— field: name, baseSalary, abstract methodcalculatePay() FullTimeEmployee— pay = baseSalaryPartTimeEmployee— เพิ่ม field hoursWorked, pay = baseSalary + hoursWorked * 50Manager extends FullTimeEmployee— เพิ่ม bonus, pay = base + bonus
ใน main: สร้างพนักงานหลายชนิด เก็บใน Employee[] วน loop print pay
📖 เฉลย
java
abstract class Employee {
protected String name; // protected = subclass ใน package เดียวกันเข้าถึงได้
protected double baseSalary; // (ของจริงควรเป็น private + getter แต่ใช้ protected เพื่อให้ตัวอย่างสั้น)
public Employee(String name, double baseSalary) {
this.name = name;
this.baseSalary = baseSalary;
}
public abstract double calculatePay();
public void printPay() {
System.out.println(name + ": " + calculatePay());
}
}
class FullTimeEmployee extends Employee {
public FullTimeEmployee(String name, double baseSalary) {
super(name, baseSalary);
}
@Override
public double calculatePay() {
return baseSalary;
}
}
class PartTimeEmployee extends Employee {
private double hoursWorked;
public PartTimeEmployee(String name, double baseSalary, double hoursWorked) {
super(name, baseSalary);
this.hoursWorked = hoursWorked;
}
@Override
public double calculatePay() {
return baseSalary + hoursWorked * 50;
}
}
class Manager extends FullTimeEmployee {
private double bonus;
public Manager(String name, double baseSalary, double bonus) {
super(name, baseSalary);
this.bonus = bonus;
}
@Override
public double calculatePay() {
return super.calculatePay() + bonus;
}
}
public class Main {
public static void main(String[] args) {
Employee[] staff = {
new FullTimeEmployee("Anna", 30000),
new PartTimeEmployee("Ben", 10000, 80),
new Manager("Cara", 50000, 20000)
};
for (Employee e : staff) e.printPay();
}
}ตอนต่อไป: interface ("สัญญา" ที่ implement ได้หลายอัน), composition over inheritance และ sealed class