Module 5 bạn tạo class Library quản lý Book và Borrower — 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: Employee và Manager đều có name, salary; Circle và Square đề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ường | Java |
|---|---|
| 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 chung | Field + method của class cha |
| Nâng cấp chip, pin | Method 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
nametừ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 object5. 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 public và protected — 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 là 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:
- JLS §8.1.4 — Superclasses and Subclasses — rule extends, đơn kế thừa.
- JLS §8.8.7.1 — Explicit Constructor Invocations — rule super(...) / this(...) phải dòng đầu.
- JLS §8.1.1.2 — final Classes — ngăn subclass.
- Object class — 11 method cơ bản mà mọi class kế thừa.
- JEP 447 — Statements before super(...) — proposal nới rule dòng đầu.
- Effective Java Item 18: "Favor composition over inheritance".
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ènsuper(). - 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 classcấ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
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();
▸
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
CKhi new C() chạy:
- Constructor
C()được gọi. Nó không viếtsuper(...)→ Java ngầm chènsuper(). B()được gọi. Tương tự, chènsuper()→A().A()gọisuper()→Object().Object()xong, quay raA(): in"A".- Quay ra
B(): in"B". - 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; }
}
▸
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; }
}
▸
class Base {
private int secret = 42;
}
class Derived extends Base {
public int expose() { return secret; }
}Không. secret là private — 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
secretthànhprotected— subclass truy xuất được trực tiếp. - Giữ
private, thêmprotected int getSecret() { return secret; }— kiểm soát access qua method.
Cách 2 thường tốt hơn — vẫn giữ encapsulation.
Q4Viết class MyString extends String được không? Vì sao?▸
class MyString extends String được không? Vì sao?Không. String là final 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
Stringhành xử mutable, phá mọi giả định của JDK (caching, hash, thread-safety). - Security:
Stringxuất hiện trong API nhạy cảm (file path, URL, class name). Subclass có thể overrideequals/hashCodegian 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; }
}
▸
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 RectangleNhư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