`extends` và `super` — kế thừa cơ bản
Cơ chế extends, constructor chain với super(...), đơn kế thừa trong Java, Object là gốc và thứ tự khởi tạo bộ nhớ từ cha xuống con.
TL;DR: Khi một class con khai báo extends class cha, nó thừa hưởng toàn bộ state và behavior hợp lệ của cha. Điểm mấu chốt nằm ở vòng đời khởi tạo: constructor class con luôn phải kích hoạt constructor của cha qua super(...) ở dòng đầu tiên. Java ép buộc một class chỉ có một cha trực tiếp và mọi class đều quy tụ về gốc Object. Nắm vững thứ tự nạp constructor giúp bạn tránh được bẫy kinh điển: rò rỉ phương thức override khi trạng thái con chưa hoàn tất.
Ở bài trước, bạn đã trừu tượng hoá các ấn phẩm cho mượn thành mẫu chung LibraryItem. Bây giờ, ta hiện thực hoá mối quan hệ đó bằng cú pháp kế thừa trong Java.
Bạn đã biết cách gom các đặc điểm chung của sách, DVD hay tài liệu số về LibraryItem. Thay vì sao chép các trường id, title qua từng class riêng lẻ, Java cung cấp từ khoá extends để biểu diễn mối quan hệ phụ thuộc này.
Cơ chế kế thừa giúp tái sử dụng mã nguồn, nhưng cũng là công cụ dễ tạo ra thiết kế giòn gãy nhất nếu áp dụng sai bản chất. Bài này làm rõ quy tắc vận hành của extends, cách super phối hợp giữa các tầng và bức tranh thứ tự khởi tạo trong bộ nhớ.
1. Analogy — Hồ sơ gốc và phụ lục chuyên biệt
Hình dung hệ thống lưu trữ hồ sơ tài sản của một thư viện:
- Hồ sơ gốc (
LibraryItem): Mọi tài sản nhập về đều phải có mã định danh, tiêu đề và năm nhập kho. Thủ thư làm sẵn một biểu mẫu tiêu chuẩn. - Phụ lục chuyên biệt (
Book): Khi nhập một cuốn sách in, thủ thư lấy biểu mẫu gốc kẹp thêm trang bổ sung ghi số trang và tác giả.
| Nghiệp vụ hồ sơ | Khái niệm Java |
|---|---|
| Biểu mẫu tiêu chuẩn | Class cha (LibraryItem) |
| Hồ sơ sách kẹp thêm phụ lục | Class con (Book extends LibraryItem) |
| Thông tin chung trên mẫu gốc | Field và method của class cha (id, title) |
| Thông tin bổ sung của sách | Field và method của class con (author, pageCount) |
Quan hệ class B extends A chỉ hợp lệ khi câu phát biểu "B là một loại A" đúng về mặt bản chất nghiệp vụ. Ví dụ, Book là một loại LibraryItem. Nếu bạn muốn viết LibraryCard extends Book, câu nói trở nên vô nghĩa; khi đó cần dùng quan hệ thành phần (composition) thay vì kế thừa.
Thử ngẫmTrong dự án bạn đang làm, có class nào đang kế thừa class khác chỉ để lấy ké 1-2 hàm tiện ích mà không thực sự là một dạng con của nó?
2. Cú pháp extends và cơ chế đơn kế thừa hoạt động ra sao?
Để kế thừa một class, bạn đặt từ khoá extends phía sau tên class con:
public class LibraryItem {
protected String id;
protected String title;
public LibraryItem(String id, String title) {
this.id = id;
this.title = title;
}
public void displaySummary() {
System.out.println("[" + id + "] " + title);
}
}
public class Book extends LibraryItem {
private String author;
public Book(String id, String title, String author) {
super(id, title);
this.author = author;
}
public void printDetails() {
displaySummary();
System.out.println("Author: " + author);
}
}
Khi sử dụng:
Book book = new Book("B-101", "Clean Code", "Robert C. Martin");
book.printDetails();
Class Book kế thừa hai trường dữ liệu cùng phương thức displaySummary() từ LibraryItem, đồng thời khai báo thêm trường dữ liệu author và phương thức chuyên biệt của mình.
2.1 Vì sao Java chỉ cho phép đơn kế thừa class?
Java không cho phép một class kế thừa trực tiếp từ nhiều class cha:
class Book extends LibraryItem, DigitalAsset { } // Compile error: multiple inheritance
Quy định này loại bỏ bài toán đa kế thừa (diamond problem): khi hai class cha cùng định nghĩa một hàm trùng tên, class con sẽ rơi vào trạng thái nhập nhằng không biết chọn nhánh nào. Để bù đắp nhu cầu tích hợp nhiều hành vi, Java sử dụng hệ thống interface.
3. Vì sao constructor con phải gọi super(...)?
Khi tạo mới một thực thể Book, trạng thái của LibraryItem (gồm id và title) phải được khởi tạo trước. Bạn thực hiện điều này bằng cách gọi super(...):
public Book(String id, String title, String author) {
super(id, title); // Must be the first statement in child constructor
this.author = author;
}
Nếu bạn không gọi super(...) tường minh, trình biên dịch Java sẽ tự động chèn lời gọi không tham số super(). Khi đó, nếu class cha không có constructor không tham số, mã nguồn sẽ báo lỗi biên dịch ngay lập tức:
class LibraryItem {
public LibraryItem(String id) { ... } // No default no-arg constructor
}
class Book extends LibraryItem {
public Book(String author) {
// Compiler inserts super(), but LibraryItem lacks no-arg -> Compile error!
}
}
3.1 Vì sao super(...) phải nằm ở dòng đầu tiên?
Quy tắc này đảm bảo nền móng của đối tượng được xây dựng hoàn tất trước khi tầng con bắt đầu thao tác. Nếu cho phép chạy code khác trước super(...), class con có thể vô tình đọc phải những trường dữ liệu của cha chưa kịp khởi tạo giá trị.
Từ Java 22 trở đi (chính thức hoá tại JEP 513 ở Java 25), Java nới lỏng cơ chế này bằng Flexible Constructor Bodies: bạn được phép kiểm tra tham số đầu vào trước lời gọi super(...), miễn là không truy cập biến tham chiếu this.
4. Thứ tự thực thi constructor trong bộ nhớ diễn ra thế nào?
Khi một câu lệnh new thực thi, JVM tiến hành theo hai pha rõ rệt:
- Pha 1 (Leo lên): Đi theo các lời gọi
super()từ con lên cha cho tới tận gốcjava.lang.Object. - Pha 2 (Chạy thân constructor): Thực thi thân hàm lần lượt từ gốc
Objecttrở ngược xuống class con.

Hãy quan sát đoạn code kiểm chứng:
class LibraryItem {
public LibraryItem() {
System.out.println("1. Init LibraryItem");
}
}
class PrintedMedia extends LibraryItem {
public PrintedMedia() {
System.out.println("2. Init PrintedMedia");
}
}
class Book extends PrintedMedia {
public Book() {
System.out.println("3. Init Book");
}
}
Kết quả in ra màn hình luôn theo thứ tự:
1. Init LibraryItem
2. Init PrintedMedia
3. Init Book
Tuyệt đối không gọi các method có thể bị ghi đè (non-final, non-private) ngay bên trong constructor của class cha.
class BaseItem {
public BaseItem() {
validate(); // Dangerous: method can be overridden by subclass
}
public void validate() { }
}
class PremiumBook extends BaseItem {
private String secretCode;
public PremiumBook() {
super();
this.secretCode = "SEC-99";
}
@Override
public void validate() {
System.out.println(secretCode.length()); // Throws NullPointerException!
}
}
Khi chạy new PremiumBook(), constructor của BaseItem gọi validate(). Do cơ chế đa hình động, phiên bản validate() của PremiumBook được kích hoạt. Tuy nhiên lúc này thân constructor con chưa chạy tới, biến secretCode vẫn mang giá trị mặc định null, gây ra lỗi dừng chương trình ngoài ý muốn.
5. Vì sao Object là gốc của mọi class?
Mọi class trong Java nếu không khai báo extends tường minh đều ngầm kế thừa từ java.lang.Object. Đây là lý do mọi đối tượng bạn tạo ra đều tự động sở hữu các phương thức nền tảng như toString(), equals(), hashCode(), và getClass().
Book book = new Book("B-1", "Refactoring", "Martin Fowler");
System.out.println(book.getClass().getSuperclass().getName());
Cây kế thừa trong Java luôn hội tụ về một gốc duy nhất, giúp bảo đảm tính đồng nhất khi truyền tham số hoặc lưu trữ đối tượng trong các cấu trúc dữ liệu tổng quát.
6. Phạm vi truy cập và từ khoá final giới hạn kế thừa thế nào?
Class con kế thừa các thành phần từ class cha tuỳ theo mức độ kiểm soát truy cập:
protected: Cho phép các class con truy cập trực tiếp các trường và phương thức của cha, bất kể class con nằm cùng hay khác package.private: Thuộc quyền sở hữu tuyệt đối của class cha; class con không thể nhìn thấy hay can thiệp trực tiếp. Nếu muốn chia sẻ dữ liệu, hãy cung cấp getter dạngprotected.super.method(): Cho phép class con gọi lại hành vi ban đầu của class cha khi đang thực hiện override, hỗ trợ mở rộng thêm logic thay vì viết lại toàn bộ.
Khi một class đã hoàn thiện thiết kế và không mong muốn bất kỳ ai mở rộng, hãy gắn từ khoá final:
public final class ImmutableBarcode {
private final String code;
public ImmutableBarcode(String code) { this.code = code; }
}
Các class then chốt của Java như String, Integer đều là final để bảo toàn tính bất biến và ngăn ngừa các lỗi bảo mật.
7. Các lỗi sai thường gặp khi kế thừa là gì?
❌ Lỗi 1: Quên gọi super(...) khi class cha không có constructor mặc định.
✅ Khai báo tường minh lời gọi super(...) kèm đầy đủ tham số tương ứng trong constructor của class con.
❌ Lỗi 2: Gọi hàm this hoặc truy cập biến instance trước super(...).
✅ Mọi thao tác với biến instance chỉ được bắt đầu sau khi dòng lệnh super(...) hoàn thành nhiệm vụ.
❌ Lỗi 3: Nhầm lẫn giữa quan hệ sở hữu (has-a) và quan hệ kế thừa (is-a).
✅ Tránh viết class Shelf extends Book. Giá sách chỉ chứa sách (Shelf chứa List<Book>), hãy ưu tiên đóng gói thay vì kế thừa bừa bãi.
8. Tóm tắt
- Từ khoá
extendsbiểu diễn quan hệ cha-con; class con nhận lại các phương thức và trường dữ liệu hợp lệ từ cha. - Java áp dụng quy tắc đơn kế thừa để tránh xung đột mã nguồn.
- Constructor con luôn phải kích hoạt constructor cha qua
super(...)ở đầu hàm; quá trình nạp dữ liệu chạy ngược từObjectxuống con. - Không bao giờ gọi hàm có thể bị override từ bên trong constructor của class cha.
- Áp dụng
finalcho các class cần khoá lại cấu trúc và bảo vệ trạng thái bất biến.
9. Tự kiểm tra
- Q1Vì sao constructor của class con bắt buộc phải gọi constructor của class cha trước tiên?
- Q2Khi nào trình biên dịch báo lỗi nếu bạn không viết lời gọi super(...) tường minh?
- Q3Thứ tự thực thi constructor khi tạo mới một đối tượng diễn ra như thế nào?
- Q4Tại sao việc gọi phương thức có thể override bên trong constructor của cha lại dẫn tới lỗi NullPointerException?
- Q5Mục đích chính của việc đánh dấu từ khoá final lên một class là gì?
Bài tiếp theo: Override và dynamic dispatch — method "cùng tên" chọn phiên bản ở runtime
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Nhận diện abstraction sai — over-engineering trap và cách sửa
Bài tiếpOverride và dynamic dispatch — đa hình ở runtime
Lộ trình Junior Java Backend Developer
Chặng 3/21 · OOP: class, kế thừa, đa hình, abstraction — bài 10/16 của chặng
Ôn phỏng vấn
Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.
Thảo luận & góp ý
Hỏi đáp về bài này