Menu

Kế thừa trong Java: extends, super và ghi đè phương thức

Cách lớp con trong Java kế thừa thuộc tính và phương thức bằng extends, gọi lớp cha qua super và ghi đè hành vi, kèm những lỗi thường gặp.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Kế thừa là dùng lại theo quan hệ "là một"

Kế thừa (inheritance) cho phép một lớp mới xây dựng dựa trên một lớp đã có. Lớp mới, gọi là lớp con (subclass), tự động có các thuộc tính và phương thức của lớp cha (superclass), rồi thêm hoặc thay đổi những gì nó cần. Bạn dùng kế thừa khi một kiểu là dạng cụ thể hơn của một kiểu khác: một Dog là một Animal, một SavingsAccount là một BankAccount.

Bạn viết nó bằng từ khóa extends. Mọi thứ public hoặc protected ở lớp cha đều dùng được ở lớp con mà không phải viết lại.

Dog không hề khai báo name hay eat(), vậy mà nó có cả hai. Đó chính là mục đích: hành vi dùng chung nằm ở một chỗ.

super: gọi tới lớp cha

Constructor của lớp con phải đảm bảo lớp cha được khởi tạo trước. Bạn làm điều đó bằng super(...), lời gọi tới constructor của lớp cha, và nó phải là câu lệnh đầu tiên trong constructor của lớp con. Nếu bạn bỏ qua, Java sẽ ngầm chèn một lời gọi tới constructor không tham số của lớp cha, và nếu lớp cha không có constructor đó thì code không biên dịch được.

Đầu ra cho thấy constructor của lớp cha chạy trước thân constructor của lớp con: việc khởi tạo đi từ đỉnh của cây phân cấp xuống dưới.

Ghi đè phương thức

Lớp con có thể thay thế một phương thức được kế thừa bằng cách định nghĩa lại nó với cùng chữ ký. Đây là ghi đè (overriding), và bạn nên luôn đánh dấu nó bằng @Override. Annotation này không bắt buộc, nhưng nó buộc trình biên dịch kiểm tra rằng bạn thực sự khớp với một phương thức của lớp cha: nó bắt được lỗi gõ sai như tostring() thay vì toString(), vốn nếu không sẽ lặng lẽ tạo ra một phương thức hoàn toàn mới.

Dù mảng được khai báo kiểu Animal, mỗi phần tử vẫn chạy speak() của riêng nó. Java chọn phương thức dựa trên đối tượng thực tế lúc chạy, không dựa trên kiểu khai báo của biến: đó là nền tảng của đa hình (polymorphism).

Gọi phiên bản của lớp cha bằng super.method()

Ghi đè không nhất thiết phải bỏ đi công việc của lớp cha. Dùng super.method() để chạy phiên bản được kế thừa rồi bổ sung thêm:

Nếu không có super., việc gọi log bên trong TimestampLogger.log sẽ gọi lại chính nó và đệ quy mãi mãi. super. nói rõ "phiên bản của lớp cha".

Thuộc tính được kế thừa và quyền truy cập

Lớp con thấy được các thành viên public và protected của lớp cha, nhưng không thấy các thành viên private. Thuộc tính private vẫn tồn tại trong đối tượng (phương thức của chính lớp cha vẫn dùng được chúng), nhưng lớp con không thể tham chiếu trực tiếp tới chúng. Hãy dùng protected, một trong các access modifier của Java, khi bạn muốn lớp con truy cập được mà vẫn giấu thành viên đó khỏi code không liên quan.

class Base {
    private int secret;      // invisible to subclasses
    protected int shared;    // visible to subclasses
}

class Derived extends Base {
    void demo() {
        shared = 5;          // OK
        // secret = 5;       // compile error - private to Base
    }
}

Đây cũng là lý do constructor của lớp con thường bắt buộc phải gọi super(...): đó là cách duy nhất để khởi tạo trạng thái private của lớp cha.

Chặn kế thừa bằng final

Đôi khi một lớp hoàn toàn không nên bị kế thừa: String là final chính vì lý do này. Đánh dấu một lớp là final sẽ cấm tạo lớp con; đánh dấu một phương thức là final sẽ cấm ghi đè nó nhưng vẫn cho phép kế thừa lớp.

final class Constants { }            // cannot be subclassed

class Config {
    final void load() { }            // subclasses may extend Config
                                     // but cannot override load()
}

Hãy dùng final khi hành vi của một lớp phải được đảm bảo và không thay đổi trong toàn chương trình: đó là tín hiệu "đừng kế thừa" có chủ đích, không phải lựa chọn mặc định.

Một lỗi thường gặp: ưu tiên composition khi không phải "là một"

Kế thừa hấp dẫn vì nó dùng lại code, nhưng nó gắn chặt lớp con với lớp cha. Nếu quan hệ không thực sự là "là một", chẳng hạn một Car cần một Engine, đừng viết Car extends Engine. Ô tô có động cơ, chứ không phải là động cơ. Hãy mô hình hóa điều đó bằng một thuộc tính (composition) thay vì kế thừa:

class Car {
    private Engine engine = new Engine();   // Car HAS-A Engine

    void start() { engine.ignite(); }
}

Chỉ dùng kế thừa khi lớp con thực sự là một dạng chuyên biệt của lớp cha và bạn muốn kế thừa cũng như thay thế được hành vi của nó.

Tiếp theo: interface

Kế thừa bằng extends cho bạn một lớp cha duy nhất và phần cài đặt dùng chung. Nhưng một lớp chỉ kế thừa được một lớp, vậy làm sao để các lớp không liên quan có chung một khả năng? Đó là việc của interface: một hợp đồng mà nhiều lớp có thể cài đặt, và cũng là chủ đề của trang tiếp theo.

Câu hỏi thường gặp

Kế thừa trong Java là gì?

Kế thừa (inheritance) cho phép một lớp (lớp con) dùng lại thuộc tính và phương thức của một lớp khác (lớp cha) bằng từ khóa extends. Lớp con tự động có các thành viên public và protected của lớp cha, có thể thêm thành viên mới hoặc thay hành vi được kế thừa bằng cách ghi đè. Nó mô hình hóa quan hệ "là một": một Dog là một Animal.

Từ khóa super trong Java dùng để làm gì?

super chỉ tới lớp cha. super(...) trong constructor gọi constructor của lớp cha (và phải là câu lệnh đầu tiên), còn super.method() gọi phiên bản của lớp cha cho một phương thức mà bạn đã ghi đè. Nó cho phép lớp con xây dựng tiếp trên logic của lớp cha thay vì thay thế hoàn toàn.

Ghi đè (overriding) và nạp chồng (overloading) trong Java khác nhau thế nào?

Ghi đè là định nghĩa lại một phương thức được kế thừa trong lớp con với cùng chữ ký để thay đổi hành vi, và nên đánh dấu bằng @Override. Nạp chồng là định nghĩa nhiều phương thức cùng tên nhưng khác danh sách tham số trong cùng một lớp. Ghi đè liên quan đến kế thừa và việc chọn phương thức lúc chạy; nạp chồng chỉ là hai phương thức tình cờ cùng tên.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu