Java — Từ Zero đến Senior/Kế thừa & Đa hình/`extends` và `super` — kế thừa cơ bản
1/7
~17 phútKế thừa & Đa hình

`extends` và `super` — kế thừa cơ bản

Cú pháp extends, constructor chain với super(...), mỗi class có đúng 1 cha, Object là gốc, và vì sao constructor cha luôn chạy trước.

Module 5 bạn tạo class Library quản lý BookBorrower — mỗi class đứng riêng. Nhưng thực tế bạn sẽ sớm gặp nhiều class chia sẻ trạng thái và hành vi: EmployeeManager đều có name, salary; CircleSquare đều có area(), perimeter(). Copy–paste field/method sang mỗi class vừa tốn công, vừa dễ lệch khi sửa.

Kế thừa (extends) là cơ chế Java cho phép một class kế thừa field và method từ class khác. Nhưng kế thừa cũng là công cụ dễ bị lạm dụng nhất trong OOP. Bài này dạy cú pháp đúng, cơ chế constructor chain, và vì sao mọi class ngầm kế thừa Object.

1. Analogy — cây gia đình

Mọi mẫu điện thoại Samsung Galaxy S23, S24, S25 đều có chung thiết kế cơ bản: màn hình OLED, nút nguồn, camera sau. Mỗi phiên bản kế thừa thiết kế cũ và thêm/sửa một số thứ — pin to hơn, chip mới, màn hình sáng hơn.

Đời thườngJava
Thiết kế cơ bản (Galaxy)Class cha (SmartPhone)
Phiên bản mới (S25)Class con (GalaxyS25 extends SmartPhone)
Bộ tính năng chungField + method của class cha
Nâng cấp chip, pinMethod override ở class con

💡 💡 Cách nhớ

class B extends A đọc là "B là một loại A". Nếu đọc câu đó thấy kỳ (vd Cart extends Book — Cart là một loại Book?), chọn composition thay vì inheritance. Bài 6 sẽ đào sâu rule này.

2. Cú pháp extends

public class Animal {
    String name;

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

    public void eat() {
        System.out.println(name + " is eating");
    }
}

public class Dog extends Animal {
    String breed;

    public Dog(String name, String breed) {
        super(name);          // goi constructor cha
        this.breed = breed;
    }

    public void bark() {
        System.out.println(name + " says woof");   // name ke thua tu Animal
    }
}

Dùng:

Dog d = new Dog("Rex", "Husky");
d.eat();    // Rex is eating — method ke thua tu Animal
d.bark();   // Rex says woof — method rieng cua Dog

Dog kế thừa:

  • Field name từ Animal.
  • Method eat() từ Animal.

Dog thêm:

  • Field breed.
  • Method bark().

2.1 Đơn kế thừa

Java chỉ cho phép extends 1 class:

class Dog extends Animal, Pet { ... }   // COMPILE ERROR — khong dang ke thua nhieu class

Lý do: tránh "diamond problem" (2 cha cùng có method foo() — con gọi phiên bản nào?). Java giải quyết bằng quy tắc này + cho phép implement nhiều interface (bài 04).

3. super(...) — gọi constructor cha

Constructor class con phải gọi constructor class cha — bằng super(...)dòng đầu tiên:

public Dog(String name, String breed) {
    super(name);          // goi Animal(String name) — phai dong dau tien
    this.breed = breed;
}

Nếu bạn không viết super(...), Java ngầm chèn super() (gọi constructor no-arg của cha):

public Dog(String breed) {
    // ngam: super();
    this.breed = breed;
}

Nhưng nếu class cha không có constructor no-arg, cách ngầm fail compile:

class Animal {
    String name;
    public Animal(String name) { this.name = name; }   // khong co constructor no-arg
}

class Cat extends Animal {
    public Cat() {
        // ngam super() -> COMPILE ERROR: there is no default constructor in Animal
    }
}

Fix: gọi super(...) rõ ràng với argument phù hợp.

3.1 Vì sao super(...) phải dòng đầu tiên?

Để đảm bảo class cha khởi tạo xong trước khi class con truy cập field cha. Cho phép code chạy trước super(...) sẽ cho phép class con truy cập field chưa khởi tạo → chaos.

public Dog(String name, String breed) {
    System.out.println(this.name);   // COMPILE ERROR neu truoc super()
    super(name);
    this.breed = breed;
}

ℹ️ 📚 Java 21 — hạn chế này được nới lỏng

JEP 447 (Java 21 preview, final ở Java 25?) cho phép code chạy trước super(...) miễn là không truy cập this. Dùng để validate argument trước khi chain cha:

public Dog(String name, String breed) {
    if (breed == null) throw new IllegalArgumentException(...);
    super(name);   // OK voi JEP 447
}

Hiện tại (Java 21 stable) vẫn yêu cầu super(...) dòng đầu. Chờ preview ổn định.

4. Thứ tự constructor chạy

Khi bạn new Dog(...), JVM chạy constructor từ gốc xuống con:

class A {
    public A() { System.out.println("A"); }
}

class B extends A {
    public B() { System.out.println("B"); }
}

class C extends B {
    public C() { System.out.println("C"); }
}

new C();

In:

A
B
C

Mỗi constructor gọi super() ngầm hoặc tường minh → cha luôn init xong trước. Object là gốc trên cùng → constructor Object() chạy trước hết.

sequenceDiagram
    participant Caller as new C()
    participant C
    participant B
    participant A
    participant Object

    Caller->>C: C()
    C->>B: super() (ngam)
    B->>A: super() (ngam)
    A->>Object: super() (ngam)
    Object-->>A: init xong
    A-->>B: init xong, in "A"
    B-->>C: init xong, in "B"
    C-->>Caller: in "C", tra object

5. Object — gốc của mọi class

Class không extends gì ngầm extends java.lang.Object:

public class Book { }
// ngam:
public class Book extends Object { }

Đó là lý do mọi class đều có toString(), equals(), hashCode(), getClass() — chúng kế thừa từ Object.

Kiểm chứng bằng reflection:

Book b = new Book();
System.out.println(b.getClass().getSuperclass());   // class java.lang.Object

Với class có extends, superclass trực tiếp là class đó:

class Dog extends Animal { }
new Dog().getClass().getSuperclass();   // class Animal

Cây kế thừa Java là cây (không phải rừng) — mọi class truy lên đủ xa đều gặp Object.

6. Truy cập field và method từ class con

6.1 publicprotected — thấy được

class Animal {
    protected String name;
    public void eat() { ... }
}

class Dog extends Animal {
    public void greet() {
        System.out.println(name);   // OK — protected thay duoc trong subclass
        eat();                       // OK — public
    }
}

6.2 private — không thấy được

class Animal {
    private int age;
    private void sleep() { ... }
}

class Dog extends Animal {
    public void demo() {
        System.out.println(age);   // COMPILE ERROR — private khong ke thua duoc
        sleep();                    // COMPILE ERROR
    }
}

private = "chỉ chính class này". Subclass không truy cập được. Muốn subclass truy cập → đổi thành protected hoặc cung cấp protected getter.

6.3 super. — gọi phiên bản của cha

Khi class con có method/field trùng tên với cha, super.x ép gọi phiên bản cha:

class Animal {
    public String describe() { return "Animal"; }
}

class Dog extends Animal {
    @Override
    public String describe() {
        return super.describe() + " (Dog)";   // goi Animal.describe
    }
}

new Dog().describe();   // "Animal (Dog)"

Pattern này phổ biến khi override: "làm thêm việc, nhưng vẫn gọi logic cha".

7. final class — cấm kế thừa

public final class String { ... }   // JDK danh dau String final

final class không thể bị extends. Dùng để:

  • Đảm bảo security/immutability (String immutable mãi mãi).
  • Đảm bảo invariant không bị subclass phá.
  • Cho JIT tối ưu (không cần lookup virtual method).
final class Money { ... }

class MoneyBag extends Money { }   // COMPILE ERROR

Effective Java khuyến nghị: class public nên final hoặc thiết kế kỹ cho kế thừa. Không có middle ground — "class mở nhưng không có doc cho subclass" là bẫy.

8. Pitfall tổng hợp

Nhầm 1: Quên super(...) khi cha không có no-arg constructor.

class A { A(String s) { ... } }
class B extends A { B() { } }   // COMPILE ERROR

public B() { super("default"); }.

Nhầm 2: Dùng this hoặc field trước super(...).

public Dog(String name) {
    this.name = name;   // COMPILE ERROR
    super(name);
}

super(...) phải dòng đầu.

Nhầm 3: Tưởng private kế thừa được.

class A { private int x; }
class B extends A { void f() { System.out.println(x); } }   // COMPILE ERROR

✅ Dùng protected hoặc getter.

Nhầm 4: class Cart extends Book vì "Cart có book".

class Book { String title; }
class Cart extends Book { List<Item> items; }   // nghia la "cart la mot loai book"?!

Composition: class Cart { List<Book> books; }. "Cart chứa book", không phải "Cart book".

Nhầm 5: Extends class final.

class MyString extends String { }   // COMPILE ERROR

✅ String/Integer/Long... là final — compose, don't extend.

9. 📚 Deep Dive Oracle

ℹ️ 📚 Deep Dive Oracle (optional)

Spec / reference chính thức:

Ghi chú: Item 18 của Effective Java là chương cần đọc nhất về OOP. Bloch kết luận: "Inheritance is appropriate only in circumstances where the subclass really is a subtype of the superclass". Khi không chắc, chọn composition.

10. Tóm tắt

  • class B extends A — B kế thừa field và method của A (trừ private).
  • Java đơn kế thừa class — 1 class chỉ có 1 cha trực tiếp.
  • Mọi class ngầm extends java.lang.Object — gốc của cây kế thừa.
  • Constructor class con phải gọi super(...)dòng đầu tiên. Không viết thì Java ngầm chèn super().
  • Nếu cha không có no-arg constructor, class con phải gọi super(...) tường minh.
  • Thứ tự init: Object → ... → cha → con.
  • super.method() để gọi phiên bản của cha khi con override.
  • final class cấm kế thừa — dùng cho immutable (String) hoặc class không thiết kế cho mở rộng.
  • Rule "is-a" — chỉ extends khi con thực sự là một loại của cha. Không chắc → composition.

11. Tự kiểm tra

Tự kiểm tra
Q1
Đoạn sau in gì?
class A {
    public A() { System.out.println("A"); }
}
class B extends A {
    public B() { System.out.println("B"); }
}
class C extends B {
    public C() { System.out.println("C"); }
}

new C();

In:

A
B
C

Khi new C() chạy:

  1. Constructor C() được gọi. Nó không viết super(...) → Java ngầm chèn super().
  2. B() được gọi. Tương tự, chèn super()A().
  3. A() gọi super()Object().
  4. Object() xong, quay ra A(): in "A".
  5. Quay ra B(): in "B".
  6. Quay ra C(): in "C".

Thứ tự: constructor chạy "ngược từ cha xuống con" — cha luôn init xong trước khi con bắt đầu.

Q2
Đoạn sau có lỗi gì?
class Animal {
    public Animal(String name) { ... }
}
class Dog extends Animal {
    String breed;
    public Dog(String breed) { this.breed = breed; }
}

Compile error: "there is no default constructor available in 'Animal'".

Dog() không viết super(...) → Java ngầm chèn super(). Nhưng Animal chỉ có constructor Animal(String name) — không có no-arg — nên super() không match.

Fix: gọi super(...) tường minh:

public Dog(String name, String breed) {
    super(name);
    this.breed = breed;
}

Bài học: nếu cha không có no-arg constructor, mọi constructor con phải gọi super(...) rõ ràng. Nếu bạn thêm constructor vào class cha cho framework (Jackson, JPA), nhớ kiểm class con vẫn compile.

Q3
Đoạn sau compile không?
class Base {
    private int secret = 42;
}
class Derived extends Base {
    public int expose() { return secret; }
}

Không. secretprivate — chỉ class Base truy cập được. Derived extends Base nhưng không kế thừa private member.

Lý do thiết kế: private là "implementation detail" — có thể đổi tự do mà không ảnh hưởng subclass. Nếu private kế thừa được, mọi đổi tên field trong class cha đều phá subclass.

Fix (chọn 1):

  • Đổi secret thành protected — subclass truy xuất được trực tiếp.
  • Giữ private, thêm protected int getSecret() { return secret; } — kiểm soát access qua method.

Cách 2 thường tốt hơn — vẫn giữ encapsulation.

Q4
Viết class MyString extends String được không? Vì sao?

Không. Stringfinal class — khai báo cấm kế thừa (JLS §8.1.1.2).

Lý do String final:

  • Immutability: nếu subclass được, subclass có thể override method làm String hành xử mutable, phá mọi giả định của JDK (caching, hash, thread-safety).
  • Security: String xuất hiện trong API nhạy cảm (file path, URL, class name). Subclass có thể override equals/hashCode gian lận để bypass check.
  • Performance: JIT tối ưu aggressive cho final class — biết chắc không ai override.

Thay thế: composition. Tạo class MyString có field private final String value + method delegate. Không extends, không lừa JVM.

Q5
Đoạn sau có hợp lý về mặt thiết kế không?
class Rectangle { int width, height; }
class Square extends Rectangle {
    public void setWidth(int w) { super.width = w; super.height = w; }
    public void setHeight(int h) { super.width = h; super.height = h; }
}

Không hợp lý — đây là ví dụ kinh điển của Liskov Substitution Principle (LSP) bị vi phạm.

Caller có Rectangle r viết code:

r.setWidth(5);
r.setHeight(10);
assert r.width == 5 && r.height == 10;   // luon dung voi Rectangle

Nhưng nếu r thực ra là Square (dynamic dispatch — bài sau), setHeight(10) cũng set width = 10 → assertion fail. Square không substitutable cho Rectangle dù toán học "hình vuông là hình chữ nhật đặc biệt".

Bài học: "is-a" trong toán học ≠ "is-a" trong LSP. Kiểm tra: mọi code dùng được với cha phải dùng được với con mà không thay đổi behaviour. Nếu không, không dùng kế thừa.

Giải pháp: không extends. Tạo 2 class riêng, hoặc interface chung Shape với method area(), perimeter(). Xem bài 6 về composition.


Bài tiếp theo: Override và dynamic dispatch — method "cùng tên" chọn phiên bản ở runtime