Skip to content

บทที่ 6.2 — Polymorphism & Abstract class

← บทที่ 6.1 | สารบัญ | บทที่ 6.3: Interface →

📚 บทที่ 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 เป็น type Animal (ต่างจาก Python/JS ที่ตัวแปรไม่ต้องประกาศ type)

2. compile-time type vs runtime type — Java แยกสอง type ของตัวแปรเดียวกัน:

  • compile-time type = type ที่ประกาศไว้ตอนเขียนโค้ด (compiler เห็นแค่นี้)
  • runtime type = type ของ object จริงที่ตัวแปรถืออยู่ตอนโปรแกรมทำงาน (อาจเป็น "ลูก" ของ compile-time type ก็ได้)

ตัวอย่าง:

java
Animal 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 ในบทนี้ใช้ private field พร้อม 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 ที่หาสาเหตุยาก ตัวอย่างจริง:

java
class 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 — ถ้าใช่จะ cast a ให้กลายเป็นตัวแปรใหม่ 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 method calculatePay()
  • FullTimeEmployee — pay = baseSalary
  • PartTimeEmployee — เพิ่ม field hoursWorked, pay = baseSalary + hoursWorked * 50
  • Manager 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

→ ไปบทที่ 6.3: Interface & Composition