Skip to content

บทที่ 6.1 — Inheritance & super (การสืบทอด)

← บทที่ 5 | สารบัญ | บทที่ 6.2: Polymorphism →

📚 บทที่ 6 (OOP ขั้นต่อ) แบ่งเป็น 3 ตอน — ตอนนี้คือ 6.1: 6.1 Inheritance · 6.2 Polymorphism & Abstract · 6.3 Interface & Composition

บทนี้ขยาย OOP จากบทที่แล้ว — เพิ่ม 3 concept ที่ทำให้โค้ดยืดหยุ่นขึ้น:

  1. Inheritance — class ใหม่ "สืบทอด" จาก class เก่า (ตอน 6.1)
  2. Polymorphism — object เดียวมีหลาย "หน้า" (ตอน 6.2)
  3. Abstraction — ซ่อนรายละเอียด เปิดแค่ "สัญญา" (ตอน 6.2–6.3)

1. ปัญหาที่ Inheritance แก้

สมมติเรามี class หลายตัวที่คล้ายกัน:

java
class Dog {
    String name;
    int age;
    void eat() { ... }
    void sleep() { ... }
    void bark() { ... }
}

class Cat {
    String name;     // ซ้ำ!
    int age;         // ซ้ำ!
    void eat() { ... }   // ซ้ำ!
    void sleep() { ... } // ซ้ำ!
    void meow() { ... }
}

class Bird {
    String name;     // ซ้ำ!
    int age;         // ซ้ำ!
    void eat() { ... }   // ซ้ำ!
    void sleep() { ... } // ซ้ำ!
    void fly() { ... }
}

ปัญหา: ซ้ำ — name, age, eat(), sleep() มีในทุก class แก้ logic ของ eat() ต้องแก้ 3 ที่ — error-prone (เกิด error ได้ง่าย)


2. Inheritance — สืบทอด

ใช้ extends ให้ class ใหม่ "เอา" ของจาก class แม่:

java
// Parent (superclass / base class)
class Animal {
    String name;
    int age;
    
    void eat() {
        System.out.println(name + " is eating");
    }
    
    void sleep() {
        System.out.println(name + " is sleeping");
    }
}

// Child (subclass / derived class)
class Dog extends Animal {
    void bark() {
        System.out.println(name + " says: Woof!");
    }
}

class Cat extends Animal {
    void meow() {
        System.out.println(name + " says: Meow!");
    }
}

⚠️ สำคัญ: ใน Java method ทุกตัว "override ได้" (overridable) โดยปริยาย — ไม่ต้องประกาศอะไรพิเศษที่ parent ก่อน ถ้าไม่อยากให้ class ลูก override method ไหน ต้องใส่ final กำกับไว้เอง (อธิบายละเอียดใน หัวข้อ 5)

ตอนนี้ Dog "ได้" name, age, eat, sleep ฟรี ๆ:

java
Dog buddy = new Dog();
buddy.name = "Buddy";
buddy.eat();    // "Buddy is eating" (มาจาก Animal)
buddy.bark();   // "Buddy says: Woof!" (ของ Dog เอง)

ภาพ:

text
       Animal
      /  |  \
    Dog Cat Bird

Dog/Cat/Bird "เป็น" Animal

คำศัพท์

คำเหล่านี้ หมายถึงสิ่งเดียวกัน เพียงแต่เรียกต่างกันในหนังสือ/เอกสารต่าง ๆ:

คำศัพท์อังกฤษความหมายไทยตัวอย่าง
Parent classคลาสแม่ / คลาสที่ถูก extendAnimal
Child classคลาสลูก / คลาสที่ extendDog
Superclassเหมือน Parent class (super = อยู่บน/แม่)Animal
Subclassเหมือน Child class (sub = อยู่ล่าง/ลูก)Dog
Base classคลาสพื้นฐาน/ต้นกำเนิด (เหมือน Parent)Animal
Derived classคลาสที่สืบทอดต่อมา (เหมือน Child)Dog

📌 จำง่าย: super = parent (คลาสแม่/อยู่บน), sub = child (คลาสลูก/อยู่ล่าง)


3. super — ของ parent

3.1 เรียก constructor ของ parent

java
class Animal {
    String name;
    int age;
    
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

class Dog extends Animal {
    String breed;
    
    public Dog(String name, int age, String breed) {
        super(name, age);    // เรียก constructor ของ Animal
        this.breed = breed;
    }
}

super(...) = "เรียก constructor ของ parent" — ต้องเป็น statement (คำสั่ง) แรกใน constructor

📖 มีข้อยกเว้นไหม (สำหรับคนอยากรู้ลึก)

Java เวอร์ชันใหม่ (22–24) มี JEP 447/482 "Flexible Constructor Bodies" ที่ผ่อนปรนกฎนี้บ้าง — แต่ยังเป็นสถานะ preview (ทดลอง ยังไม่ใช่ standard) ในทั้ง 3 เวอร์ชัน หนังสือนี้แนะนำให้ยึด super() เป็นบรรทัดแรกเสมอ เพื่อความชัดเจนและใช้ได้กับ Java ทุกเวอร์ชัน

3.2 เรียก method ของ parent

java
class Dog extends Animal {
    @Override
    void eat() {
        super.eat();   // เรียก eat ของ Animal ก่อน
        System.out.println("  + dog-specific eating sound");
    }
}

3.3 ⚠️ ถ้า parent ไม่มี default constructor

java
class Animal {
    public Animal(String name) { ... }    // ไม่มี Animal() เปล่า ๆ
}

class Dog extends Animal {
    public Dog() {
        // ❌ error: no default constructor in Animal
    }
}

Java จะเรียก super() ให้อัตโนมัติถ้าเราไม่เขียน — แต่ถ้า parent ไม่มี default → error แก้: เรียก super("...") ที่ตรงกับ constructor ของ parent


4. Override — ลบล้าง method ของ parent

Child เปลี่ยน behavior ของ method ที่สืบทอดได้:

java
class Animal {
    void makeSound() {
        System.out.println("Some generic sound");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Woof!");
    }
}

class Cat extends Animal {
    @Override
    void makeSound() {
        System.out.println("Meow!");
    }
}

ใช้งาน:

java
Dog d = new Dog();
d.makeSound();    // "Woof!" (ใช้ของ Dog)

Cat c = new Cat();
c.makeSound();    // "Meow!" (ใช้ของ Cat)

4.1 @Override annotation — สำคัญมาก

java
@Override
void makeSound() { ... }

@Override = บอก compiler ว่า "ฉันจะ override method ของ parent"

ถ้าสะกดผิด:

java
@Override
void makeSoun() { ... }    // ❌ error: method ไม่มีใน parent

— compiler จับให้ ไม่ใช่กลายเป็น method ใหม่เงียบ ๆ

⚠️ ข้อควรระวัง: @Override เป็นแค่ annotation ที่ ไม่บังคับ — ไม่ใส่ก็ compile ผ่าน โค้ดในหัวข้อ 4 ก่อนหน้านี้ (makeSound()) ทำงานถูกทั้งที่ไม่มี @Override มาก่อนก็ได้ แต่ ถ้าสะกดชื่อ method ผิด (เช่น makeSoun) โดยไม่มี @Override — Java จะไม่เตือนอะไรเลย มันจะกลายเป็น method ใหม่เงียบ ๆ (ไม่ได้ override จริง) แล้วบั๊กจะไปโผล่ตอน runtime แทน นี่คือเหตุผลที่ต้องใส่ @Override ทุกครั้ง — ให้ compiler ช่วยจับตอน compile แทนที่จะปล่อยให้พังตอนรัน

กฎ: ใส่ @Override ทุกครั้งที่ override

4.2 กฎ override

  • ชื่อ + parameter ต้องตรงกับ parent
  • return type ต้องเหมือนหรือ subtype (covariant return = "ผลลัพธ์แคบลงได้" — child คืน type ที่เฉพาะกว่า parent ได้ — ดูรายละเอียดใน 4.3)
  • access modifier ต้องเปิดเท่าเดิมหรือกว้างขึ้น — ลดความ visible ของ method ตอน override ไม่ได้ ลำดับจากแคบ → กว้าง: privatepackage-privateprotectedpublic
  • ห้าม override static method — ที่ดูเหมือน override คือ method hiding (ซ่อน method แทนที่จะ override จริง) — รายละเอียดอยู่ในบทที่ 6.2 เพราะต้องใช้ความเข้าใจ polymorphism ก่อน
  • ห้าม override final method หรือ private method (private ไม่ inherit อยู่แล้ว)
  • ห้าม throw checked exception ที่ parent ไม่ throw (เดี๋ยวบทที่ 8)

4.3 Covariant return type — ตัวอย่าง

📖 คำว่า "covariant return": "co-variant" = "ไปทางเดียวกัน" — ใน inheritance "subtype/แคบกว่า" หมายถึง class ลูก เช่น Dog เป็น Animal (Dog คือชนิดย่อยของ Animal) → ดังนั้น Dog แคบกว่า Animal — return Dog แทน Animal ได้ เพราะ Dog ก็ยังเป็น Animal อยู่

Java 5+ อนุญาตให้ child override method โดย return type แคบกว่า (subtype) ของ parent ได้:

java
class Animal {
    Animal copy() {
        return new Animal();
    }
}

class Dog extends Animal {
    @Override
    Dog copy() {            // ✅ return Dog (subtype ของ Animal) — covariant
        return new Dog();
    }
}

Dog d = new Dog().copy();   // ไม่ต้อง cast — ได้ Dog ตรง ๆ

ใช้บ่อยใน clone(), factory method, builder — caller ได้ type ที่ละเอียดขึ้น

4.4 Liskov Substitution Principle (LSP)

หลักการ OOP ที่ลึก แต่สำคัญ — ภาษาชาวบ้าน: "ถ้าแม่ทำได้ ลูกต้องทำได้เหมือนกัน โดยไม่ทำให้ของที่ทำงานกับแม่แตก"

"Subtype ต้องใช้แทน supertype ได้ โดยไม่ทำให้ behavior แตก"

ถ้าโค้ดทำงานกับ Animal ได้ — เปลี่ยนเป็น Dog ต้องยังถูกต้อง

ตัวอย่างที่ผิด LSP — Rectangle/Square

⚠️ ก่อนอ่านโค้ดนี้: ตัวอย่างด้านล่างใช้ assert — คำสั่งสำหรับตรวจสอบว่า "ต้องได้ผลนี้" (ใช้สำหรับทดสอบเท่านั้น ไม่ใช้ใน production) Java ปิด assert ไว้ตามค่าเริ่มต้นเพื่อความเร็ว — ต้องรันด้วย flag -ea (ย่อจาก enable assertions = เปิดใช้งาน assert) ไม่งั้น assert จะถูกข้ามไปเงียบ ๆ และโปรแกรมดูเหมือนทำงานถูกทั้งที่ Square พังแล้ว:

java -ea Main
java
class Rectangle {
    int width, height;
    void setWidth(int w) { this.width = w; }
    void setHeight(int h) { this.height = h; }
    int area() { return width * height; }
}

class Square extends Rectangle {
    @Override
    void setWidth(int w) {
        this.width = w;
        this.height = w;       // square = ทุกด้านเท่ากัน
    }
    @Override
    void setHeight(int h) {
        this.width = h;
        this.height = h;
    }
}

// ❌ โค้ดที่คิดว่ารับ Rectangle (illustration — ต้องอยู่ใน class จึงจะ compile ได้)
void test(Rectangle r) {
    r.setWidth(5);
    r.setHeight(10);
    assert r.area() == 50;     // Rectangle → ผ่าน, Square → 100 → FAIL
}

📝 assert ใช้สำหรับ development/testing เท่านั้น — ใน production code ใช้ throw new IllegalArgumentException(...) หรือ Objects.requireNonNull(...) แทน

Square extends Rectangle ดู natural แต่ผิด LSP — Square ทำลาย invariant ของ Rectangle (setWidth ไม่กระทบ height)

📖 กฎที่มาคู่กัน (สำหรับคนอยากเจาะลึกหลักการนี้)
  • Preconditions (เงื่อนไขก่อนเรียก) ไม่เข้มขึ้น — child รับ input ได้กว้างเท่าหรือกว้างกว่า parent
    • ตัวอย่าง: ถ้า parent รับ "อายุ 0-150 ปี" → child ต้องรับช่วงนี้ทั้งหมด ไม่ใช่จำกัด "18-60 ปี"
  • Postconditions (เงื่อนไขหลังเรียก) ไม่หลวมลง — child รับประกัน (guarantee) ผลลัพธ์อย่างน้อยเท่าเดิม
    • ตัวอย่าง: ถ้า parent คืน "list ที่ sort แล้ว" → child ก็ต้องคืน sort แล้วเหมือนกัน ไม่ใช่ "อาจ sort หรือไม่"
  • Invariants (กฎคงที่ของ class) ของ parent ต้องคงอยู่ใน child
    • ตัวอย่าง Rectangle: invariant = "width กับ height เป็นอิสระต่อกัน" → Square ทำลายเพราะ width = height เสมอ

💡 จำง่าย ๆ ว่า: ถ้า override แล้วต้องเขียน comment/Javadoc ว่า "ทำต่างจากแม่" → มักผิด LSP — อาจไม่ควร extend ใช้ composition (เรียนบทที่ 6.3) แทน


5. final กับ inheritance

java
final class String { ... }      // ห้าม extend
final void method() { ... }     // ห้าม override

ใช้กับ class/method ที่ "ไม่ออกแบบมาให้ extend"

String ของ Java เป็น final — ป้องกัน security issue (ลองนึกถ้าใครก็ extend String ได้)


6. Object class — class แม่ของทุกอย่าง

ทุก class ใน Java extend Object โดยปริยาย:

java
class Dog { ... }
// เหมือนกับ
class Dog extends Object { ... }

Object มี method ติดมา:

  • toString()
  • equals(Object)
  • hashCode()
  • getClass()
  • clone() (ไม่แนะนำ — มีปัญหาการ copy ที่ซับซ้อน ควรใช้ copy constructor หรือ static factory method แทน)
  • notify(), wait() (concurrency เก่า — ปัจจุบันใช้ java.util.concurrent package แทน)

📌 finalize() เคยเป็น method ของ Object ในยุคก่อน — ถูก deprecated ตั้งแต่ Java 9 และ ลบออกตั้งแต่ Java 18 (JEP 421) อย่าเขียนโค้ดใหม่ที่ใช้ finalize แต่อาจเจอใน codebase เก่า

นี่คือเหตุผลที่ method equals(Object obj) รับ Object — เพราะอะไรก็เป็น Object ได้



✅ Checkpoint — ลองเขียนเอง

ก่อนไปบทถัดไปลองทำตามนี้:

  1. สร้าง class Animal ที่มี constructor รับ name และ age พร้อม method makeSound()
  2. สร้าง class Dog และ Cat ที่ extends Animal — ใช้ super(name, age) ใน constructor
  3. @Override method makeSound() ให้ Dog พิมพ์ "Woof!" และ Cat พิมพ์ "Meow!"
  4. สร้าง main เพื่อ new Dog และ Cat แล้วเรียก makeSound() — ตรวจสอบว่าแต่ละตัวพิมพ์เสียงของตัวเอง
📖 เฉลย
java
class Animal {
    String name;
    int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    void makeSound() {
        System.out.println(name + " makes a sound");
    }
}

class Dog extends Animal {
    public Dog(String name, int age) {
        super(name, age);
    }

    @Override
    void makeSound() {
        System.out.println(name + " says: Woof!");
    }
}

class Cat extends Animal {
    public Cat(String name, int age) {
        super(name, age);
    }

    @Override
    void makeSound() {
        System.out.println(name + " says: Meow!");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog buddy = new Dog("Buddy", 3);
        Cat whiskers = new Cat("Whiskers", 5);

        buddy.makeSound();     // Buddy says: Woof!
        whiskers.makeSound();  // Whiskers says: Meow!
    }
}

ตอนต่อไป: เอา inheritance มาต่อยอดเป็น polymorphism (object เดียวหลายร่าง) และ abstract class

→ ไปบทที่ 6.2: Polymorphism & Abstract class