โหมดมืด
บทที่ 6.1 — Inheritance & super (การสืบทอด)
📚 บทที่ 6 (OOP ขั้นต่อ) แบ่งเป็น 3 ตอน — ตอนนี้คือ 6.1: 6.1 Inheritance · 6.2 Polymorphism & Abstract · 6.3 Interface & Composition
บทนี้ขยาย OOP จากบทที่แล้ว — เพิ่ม 3 concept ที่ทำให้โค้ดยืดหยุ่นขึ้น:
- Inheritance — class ใหม่ "สืบทอด" จาก class เก่า (ตอน 6.1)
- Polymorphism — object เดียวมีหลาย "หน้า" (ตอน 6.2)
- 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 BirdDog/Cat/Bird "เป็น" Animal
คำศัพท์
คำเหล่านี้ หมายถึงสิ่งเดียวกัน เพียงแต่เรียกต่างกันในหนังสือ/เอกสารต่าง ๆ:
| คำศัพท์อังกฤษ | ความหมายไทย | ตัวอย่าง |
|---|---|---|
| Parent class | คลาสแม่ / คลาสที่ถูก extend | Animal |
| Child class | คลาสลูก / คลาสที่ extend | Dog |
| 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 ไม่ได้ ลำดับจากแคบ → กว้าง:
private→package-private→protected→public - ห้าม override
staticmethod — ที่ดูเหมือน override คือ method hiding (ซ่อน method แทนที่จะ override จริง) — รายละเอียดอยู่ในบทที่ 6.2 เพราะต้องใช้ความเข้าใจ polymorphism ก่อน - ห้าม override
finalmethod หรือprivatemethod (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.concurrentpackage แทน)
📌
finalize()เคยเป็น method ของ Object ในยุคก่อน — ถูก deprecated ตั้งแต่ Java 9 และ ลบออกตั้งแต่ Java 18 (JEP 421) อย่าเขียนโค้ดใหม่ที่ใช้ finalize แต่อาจเจอใน codebase เก่า
นี่คือเหตุผลที่ method equals(Object obj) รับ Object — เพราะอะไรก็เป็น Object ได้
✅ Checkpoint — ลองเขียนเอง
ก่อนไปบทถัดไปลองทำตามนี้:
- สร้าง class
Animalที่มี constructor รับnameและageพร้อม methodmakeSound() - สร้าง class
DogและCatที่extends Animal— ใช้super(name, age)ใน constructor @OverridemethodmakeSound()ให้ Dog พิมพ์ "Woof!" และ Cat พิมพ์ "Meow!"- สร้าง
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