Java OO & Functional
4/38
Bài 4 / 38~12 phútKế thừa & Đa hìnhLộ trình · chặng 03/21Miễn phí… lượt xem

Override và dynamic dispatch — đa hình ở runtime

Cơ chế override method, phân biệt với overload, annotation @Override, dynamic dispatch với vtable trong JVM, covariant return type và các quy tắc Liskov.

TL;DR: Khi class con kế thừa từ cha, nó có thể ghi đè (override) phương thức để thay đổi hành vi nhưng vẫn giữ nguyên chữ ký gọi. Khi một biến kiểu cha trỏ tới đối tượng con tại runtime, JVM sử dụng dynamic dispatch tra bảng vtable để thực thi đúng phiên bản của đối tượng thực tế trên Heap. Hiểu rõ cơ chế này giúp bạn phân biệt rạch ròi giữa override (đa hình động lúc chạy) và overload (đa hình tĩnh lúc biên dịch), đồng thời tuân thủ các quy tắc bảo toàn tính thay thế của Liskov (LSP).

Nhớ lại — Kế thừa và super

Ở bài trước, bạn đã dùng extends để class con Book thừa hưởng các thuộc tính và phương thức từ LibraryItem. Bây giờ, ta tìm hiểu cách class con thay đổi hành vi của các phương thức kế thừa đó.

Trong hệ thống thư viện, mỗi ấn phẩm đều có thao tác calculateLateFee(int daysOverdue). Tuy nhiên, sách in có mức phí phạt theo ngày khác với đĩa DVD, trong khi tài liệu số (Ebook) lại tự động thu hồi quyền đọc khi hết hạn nên phí phạt luôn bằng 0.

Nếu phải viết từng hàm calculateBookFee(), calculateEbookFee() riêng rẽ, người quản lý sẽ không thể xử lý danh sách ấn phẩm một cách tổng quát. Cơ chế Override và Dynamic Dispatch giải quyết bài toán này: bạn chỉ cần gọi item.calculateLateFee(days) trên danh sách chung List<LibraryItem>, JVM sẽ tự động chọn đúng thuật toán phù hợp cho từng loại ấn phẩm tại thời điểm chạy.

1. Analogy — Biểu phí dịch vụ theo từng hạng mục

Hình dung quầy lễ tân thư viện có quy định chung: "Mọi tài sản mượn quá hạn đều phải thanh toán phí phạt".

  1. Biểu mẫu chung (LibraryItem): Có mục "Tính phí trễ hạn", áp dụng mức phí tiêu chuẩn 2.000đ/ngày.
  2. Quy định sách in (PrintedBook): Thủ thư ghi đè mức phí thành 5.000đ/ngày do tốn chi phí bảo quản và nhắc nhở vật lý.
  3. Quy định tài liệu số (DigitalEbook): Bản ghi điện tử tự động khoá file khi hết hạn, mức phí được ghi đè bằng 0đ.
Thực tế thư việnKhái niệm Java
Tên thủ tục "Tính phí trễ hạn"Chữ ký phương thức (method signature)
Quy định biểu phí mặc địnhPhương thức gốc ở class cha (LibraryItem)
Biểu phí chuyên biệt của sách in/EbookPhương thức override ở class con
Lễ tân nhìn ấn phẩm thực tế để tính tiềnDynamic dispatch chọn hàm theo kiểu thực tế trên Heap
💡 Nguyên tắc cốt lõi

Override nghĩa là giữ nguyên giao diện gọi (tên hàm, tham số) nhưng cung cấp nội dung thực thi mới. Đối tượng thực sự trên Heap là gì thì phương thức của lớp đó sẽ được thực thi.

Thử ngẫmNếu bạn có một danh sách hỗn hợp gồm 100 ấn phẩm khác nhau, làm sao chương trình tính đúng tổng tiền phạt mà không cần dùng bất kỳ câu lệnh if-else hay instanceof nào?

2. Override hoạt động như thế nào trong Java?

Quan sát cách các class con thay đổi phương thức tính phí:

Java
public class LibraryItem {
    protected String id;
    protected String title;

    public LibraryItem(String id, String title) {
        this.id = id;
        this.title = title;
    }

    public long calculateLateFee(int daysOverdue) {
        return daysOverdue * 2000L; // Phi tieu chuan: 2.000d/ngay
    }
}

public class PrintedBook extends LibraryItem {
    public PrintedBook(String id, String title) {
        super(id, title);
    }

    @Override
    public long calculateLateFee(int daysOverdue) {
        return daysOverdue * 5000L; // Phi sach in: 5.000d/ngay
    }
}

public class DigitalEbook extends LibraryItem {
    public DigitalEbook(String id, String title) {
        super(id, title);
    }

    @Override
    public long calculateLateFee(int daysOverdue) {
        return 0L; // Tai lieu so tu thu hoi, khong phat tien
    }
}

Khi xử lý danh sách mượn:

Java
LibraryItem item1 = new PrintedBook("B-101", "Clean Code");
LibraryItem item2 = new DigitalEbook("E-202", "Effective Java");

System.out.println(item1.calculateLateFee(3)); // In ra: 15000
System.out.println(item2.calculateLateFee(3)); // In ra: 0

Dù cả hai biến item1 và item2 đều có kiểu khai báo là LibraryItem, khi gọi calculateLateFee(3), kết quả trả về hoàn toàn khác nhau. JVM nhận diện đối tượng thực tế trên Heap là PrintedBook và DigitalEbook để kích hoạt đúng phiên bản logic.

2.1 Vì sao nên luôn sử dụng annotation @Override?

Annotation @Override không bắt buộc để code chạy, nhưng là chốt chặn an toàn hàng đầu:

  • Bảo vệ chữ ký: Nếu bạn gõ nhầm tên phương thức (calculateLatefee chữ f thường) hoặc đổi kiểu tham số, trình biên dịch sẽ báo lỗi ngay lập tức thay vì vô tình tạo ra một phương thức mới.
  • Dễ bảo trì: Khi ai đó sửa đổi chữ ký ở class cha, tất cả các class con có @Override sẽ báo lỗi biên dịch, giúp bạn sửa toàn bộ hệ thống ngay lập tức mà không để lọt bug ngầm.

3. Override khác Overload ở những điểm nào?

Đây là hai hình thức đa hình hoàn toàn tách biệt trong ngôn ngữ Java:

Tiêu chíMethod Overriding (Ghi đè)Method Overloading (Nạp chồng)
Thời điểm quyết địnhRuntime (Đa hình động do JVM tra bảng)Compile-time (Đa hình tĩnh do javac quyết định)
Vị trí khai báoNằm ở hai lớp có quan hệ kế thừa (Cha - Con)Nằm trong cùng một lớp (hoặc kế thừa thêm)
Chữ ký hàmBắt buộc giống hệt (tên hàm và kiểu tham số)Bắt buộc khác tham số (số lượng hoặc kiểu)
Kiểu trả vềPhải giống nhau hoặc là lớp con (Covariant)Có thể khác nhau tuỳ ý
Hiệu năngCần một bước tra bảng vtableTrình biên dịch liên kết cứng địa chỉ hàm
Java
public class LibraryService {
    // 1. Phuong thuc co ban
    public void processReturn(LibraryItem item) {
        System.out.println("Xu ly tra an pham: " + item.calculateLateFee(1));
    }

    // 2. Overload: Cung ten, khac danh sach tham so
    public void processReturn(LibraryItem item, boolean waiveFee) {
        long fee = waiveFee ? 0 : item.calculateLateFee(1);
        System.out.println("Xu ly tra an pham voi mien phi: " + fee);
    }
}

Ở ví dụ trên, hai phương thức processReturn là Overload của nhau. Ngay khi biên dịch, javac đã biết chính xác vị trí bytecode cần nhảy tới dựa trên việc bạn truyền vào 1 hay 2 đối số.

4. Cơ chế Dynamic Dispatch trong bộ nhớ JVM diễn ra thế nào?

Một lời gọi phương thức đa hình trải qua hai pha quyết định tại hai thời điểm:

Hai cột đối chiếu: lúc biên dịch javac chốt chữ ký theo kiểu khai báo, lúc chạy JVM tra vtable theo lớp thật và chốt thân method

  1. Pha 1 — Compile-time (javac): Trình biên dịch kiểm tra kiểu khai báo của biến (ở đây là LibraryItem). Nó xác nhận LibraryItem có phương thức calculateLateFee(int), kiểm tra tính hợp lệ về kiểu, rồi sinh ra mã lệnh bytecode invokevirtual.
  2. Pha 2 — Runtime (JVM): Khi bytecode invokevirtual chạy, JVM nhìn vào con trỏ header của đối tượng thực tế trên Heap để tìm bảng vtable (virtual method table) của class đó:
    • Nếu đối tượng là PrintedBook, slot calculateLateFee trong vtable trỏ thẳng tới mã máy của PrintedBook.calculateLateFee.
    • Nếu đối tượng là DigitalEbook, slot đó trỏ tới DigitalEbook.calculateLateFee.
📚 Tối ưu hoá của JIT Compiler

Việc tra vtable tốn thêm một bước đọc bộ nhớ gián tiếp. Tuy nhiên, JIT Compiler của JVM rất thông minh: nếu nó phát hiện trong 99% trường hợp tại một điểm gọi chỉ có duy nhất một class cụ thể xuất hiện (monomorphic call site), JIT sẽ tiến hành inline trực tiếp mã máy của class đó, xoá bỏ hoàn toàn chi phí tra bảng vtable.

5. Những quy tắc bắt buộc khi thực hiện Override là gì?

Để bảo toàn nguyên lý thay thế Liskov (LSP), Java đặt ra 4 quy tắc nghiêm ngặt:

5.1 Không được thu hẹp phạm vi truy cập (Visibility)

Class con chỉ có thể giữ nguyên hoặc mở rộng phạm vi truy cập, tuyệt đối không được thu hẹp:

Java
public class LibraryItem {
    public void display() { }
}

public class PrintedBook extends LibraryItem {
    protected void display() { } // Loi bien dich: attempting to assign weaker access privileges
}

Lý do: Một đoạn mã khác đang nắm giữ biến LibraryItem item có quyền kỳ vọng gọi được item.display(). Nếu class con thu hẹp thành protected hay private, lời gọi sẽ bị từ chối, phá vỡ tính tương thích.

5.2 Không được ném thêm Checked Exception mới

Class con chỉ được phép ném cùng loại exception, ném exception con của cha, hoặc không ném exception nào:

Java
public class LibraryItem {
    public void loadData() throws IOException { }
}

public class PrintedBook extends LibraryItem {
    // Hop le: khong nem exception nao, hoac chi nem FileNotFoundException (con cua IOException)
    @Override
    public void loadData() throws FileNotFoundException { }

    // Loi bien dich: SQLException khong phai con cua IOException
    // @Override
    // public void loadData() throws SQLException { }
}

5.3 Cho phép kiểu trả về hiệp biến (Covariant Return Type)

Từ Java 5 trở đi, phương thức con được phép trả về một kiểu dữ liệu con (subtype) của kiểu dữ liệu cha:

Java
public class LibraryItem {
    public LibraryItem createCopy() {
        return new LibraryItem(this.id, this.title);
    }
}

public class PrintedBook extends LibraryItem {
    @Override
    public PrintedBook createCopy() { // Tra ve PrintedBook thay vi LibraryItem
        return new PrintedBook(this.id, this.title);
    }
}

Nhờ cơ chế này, người gọi phương thức trên đối tượng PrintedBook có thể nhận ngay về đối tượng kiểu PrintedBook mà không cần ép kiểu thủ công.

5.4 Phương thức static, final và private không thể override

  • final: Đánh dấu ngăn chặn class con ghi đè logic then chốt.
  • private: Thuộc phạm vi đóng gói nội bộ của cha, class con không thể nhìn thấy nên không thể override.
  • static: Gắn liền với class chứ không gắn với thực thể đối tượng. Nếu con viết hàm static trùng tên, đó chỉ là hiện tượng che khuất (method hiding) chứ không có đa hình động.

6. Gọi lại logic của lớp cha bằng super.method()

Khi bạn chỉ muốn mở rộng thêm hành vi thay vì thay thế hoàn toàn logic ban đầu, hãy sử dụng từ khoá super:

Java
public class LibraryItem {
    public String getAuditLog() {
        return "Item: " + id;
    }
}

public class PrintedBook extends LibraryItem {
    private int shelfNumber;

    @Override
    public String getAuditLog() {
        // Goi logic goc cua cha va noi them thong tin ke sach
        return super.getAuditLog() + " | Ke: " + shelfNumber;
    }
}

Lời gọi super.getAuditLog() bỏ qua cơ chế dynamic dispatch và thực thi trực tiếp phiên bản phương thức của class cha.

7. Các lỗi sai thường gặp khi sử dụng đa hình

❌ Lỗi 1: Quên annotation @Override và gõ nhầm tên phương thức.
✅ Luôn gắn @Override để trình biên dịch đóng vai trò người kiểm duyệt chữ ký tự động.

❌ Lỗi 2: Tưởng rằng phương thức static có thể đa hình lúc chạy.
✅ Phương thức static được liên kết tĩnh lúc biên dịch theo kiểu khai báo của biến, không phụ thuộc vào đối tượng thực tế trên Heap.

❌ Lỗi 3: Ép kiểu ngược (Downcasting) không an toàn khi xử lý đối tượng đa hình.
✅ Ưu tiên thiết kế phương thức chung ở class cha để tận dụng dynamic dispatch; nếu buộc phải ép kiểu, hãy sử dụng pattern matching if (item instanceof PrintedBook book) của Java 16+.

8. Tóm tắt

  • Override cho phép class con định nghĩa lại hành vi nhưng giữ nguyên chữ ký hàm, phục vụ đa hình lúc chạy.
  • Dynamic Dispatch sử dụng bảng vtable của đối tượng thực tế trên Heap để kích hoạt phương thức tại runtime.
  • Overload phân giải lúc biên dịch dựa trên số lượng và kiểu tham số truyền vào.
  • Tuân thủ 4 quy tắc override: không thu hẹp visibility, không ném thêm checked exception mới, hỗ trợ covariant return, và không thể override hàm static/final/private.
  • Sử dụng super.method() khi cần tái sử dụng và mở rộng logic nền tảng từ class cha.

9. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Điểm khác biệt căn bản giữa Dynamic Binding (liên kết động) và Static Binding (liên kết tĩnh) là gì?
  2. Q2
    Vì sao Java cấm class con thu hẹp phạm vi truy cập (visibility) khi override phương thức của cha?
  3. Q3
    Covariant return type là gì và mang lại lợi ích gì cho lập trình viên?
  4. Q4
    Nếu bạn khai báo phương thức static trùng tên và tham số ở cả class cha lẫn class con, điều gì sẽ xảy ra?
  5. Q5
    Lợi ích lớn nhất của annotation @Override trong thực tế là gì?

Bài tiếp theo: Abstract class — lớp trừu tượng và thiết kế khung sườn

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.

Lộ trình Junior Java Backend Developer

Chặng 3/21 · OOP: class, kế thừa, đa hình, abstraction — bài 11/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

Bài tiếp theo

Abstract class — template buộc subclass hoàn thiện