CodeGym /Các khóa học /JAVA 25 SELF /Lỗi với kế thừa và nạp chồng phương thức

Lỗi với kế thừa và nạp chồng phương thức

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Lỗi khi kế thừa

Kế thừa là một trong những nền tảng của lập trình hướng đối tượng (OOP), nhưng cũng là chủ đề mà người mới bắt đầu rất hay vấp phải rắc rối. Hãy xem các lỗi kinh điển và cách tránh chúng.

Không gọi constructor của lớp cơ sở (super(...))

Khi bạn tạo lớp con, cần nhớ: lớp cơ sở có thể yêu cầu khởi tạo nhất định thông qua constructor. Nếu lớp cơ sở không có constructor mặc định (không tham số), thì trong constructor của lớp con bắt buộc phải gọi tường minh constructor của lớp cơ sở bằng super(...).

Ví dụ lỗi:

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

class Dog extends Animal {
    // Lỗi! Animal không có constructor mặc định
    public Dog() {
        // super(); // trình biên dịch tự chèn super() tự động, nhưng không có constructor như vậy!
    }
}

Cách sửa:

class Dog extends Animal {
    public Dog(String name) {
        super(name); // Ổn rồi!
    }
}

Ghi chú:
Nếu lớp cơ sở chỉ có constructor có tham số, trình biên dịch sẽ không tự thêm constructor không tham số. Đây là nguyên nhân phổ biến gây lỗi biên dịch.

Cố kế thừa từ lớp final hoặc ghi đè phương thức final

Trong Java, có thể khai báo lớp hoặc phương thức là final. Điều đó có nghĩa:

  • Không thể kế thừa lớp đó.
  • Không thể ghi đè (override) phương thức đó trong lớp con.

Ví dụ lỗi:

final class Cat {}

// Lỗi biên dịch!
class Tiger extends Cat { 
    // ...
}
class Animal {
    public final void sleep() {
        System.out.println("Zzz...");
    }
}

class Dog extends Animal {
    // Lỗi biên dịch!
    @Override
    public void sleep() {
        System.out.println("Dog is sleeping...");
    }
}

Ghi chú:
Nếu thấy lỗi "cannot inherit from final", "cannot override final method" — hãy kiểm tra lại các modifier!

Vi phạm nguyên lý thay thế Liskov (Liskov Substitution Principle)

Nghe có vẻ ghê gớm, nhưng trên thực tế nghĩa là: đối tượng của lớp con phải cư xử giống đối tượng của lớp cơ sở, không phá vỡ logic chương trình. Lỗi thường gặp — ghi đè phương thức theo cách khiến lớp mới hành xử khác với kỳ vọng từ lớp cơ sở.

Ví dụ:

class Bird {
    public void fly() {
        System.out.println("Tôi đang bay!");
    }
}

class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Chim cánh cụt không biết bay!");
    }
}

Vấn đề là gì?
Mã làm việc với Bird kỳ vọng rằng bất kỳ con chim nào cũng bay được. Nhưng nếu truyền vào Penguin, chương trình có thể ném lỗi và dừng.

Cách tốt hơn:
Trong các trường hợp như vậy, nên xem lại hệ phân cấp hoặc dùng interface/composition.

2. Lỗi với nạp chồng phương thức (overloading)

Nạp chồng là khi trong một lớp có nhiều phương thức cùng tên nhưng khác tham số. Tưởng chừng đơn giản, nhưng vẫn có thể rơi vào bẫy.

Nạp chồng thay vì ghi đè (sai chữ ký phương thức)

Người mới thường muốn ghi đè (override) phương thức của lớp cơ sở, nhưng vô tình thay đổi tham số của nó. Kết quả là nạp chồng chứ không phải ghi đè — và tính đa hình không hoạt động!

Ví dụ lỗi:

class Animal {
    public void makeSound() {
        System.out.println("Some sound");
    }
}

class Dog extends Animal {
    // Muốn ghi đè nhưng lại nạp chồng!
    public void makeSound(String extra) {
        System.out.println("Bark! " + extra);
    }
}

Vấn đề:
Gọi dog.makeSound() sẽ gọi phương thức của lớp cha, không phải phương thức mới của bạn.
Gọi dog.makeSound("loudly") sẽ gọi phương thức nạp chồng, nhưng đa hình không hoạt động!

Thực tiễn tốt nhất:
Hãy dùng chú thích @Override — nếu bạn sai chữ ký, trình biên dịch sẽ báo ngay.

@Override
public void makeSound() { /* ... */ }

Hành vi không rõ ràng khi nạp chồng (tự động ép kiểu, lời gọi mơ hồ)

Đôi khi Java có thể “chọn” không đúng phương thức bạn kỳ vọng nếu tham số khớp với nhiều phiên bản nạp chồng.

public class OverloadDemo {
    public void print(int x) {
        System.out.println("int: " + x);
    }
    public void print(double x) {
        System.out.println("double: " + x);
    }
}

OverloadDemo demo = new OverloadDemo();
demo.print(5);     // int: 5
demo.print(5.0);   // double: 5.0
demo.print(5L);    // long -> double: double: 5.0

Vấn đề:
Nếu gọi demo.print(5L), Java sẽ chọn print(double x) (vì long ép sang double “tốt” hơn sang int).
Nếu có các phương thức với tham số kiểu Object, Integer, int, lời gọi với null có thể dẫn đến lỗi biên dịch: “reference to print is ambiguous”.

Dùng cùng tên phương thức nhưng chỉ khác kiểu trả về (lỗi biên dịch)

Trong Java, không thể khai báo hai phương thức cùng tên và cùng danh sách tham số mà chỉ khác kiểu trả về!

public class Demo {
    // Lỗi biên dịch!
    public int foo() { return 1; }
    public String foo() { return "hello"; }
}

Giải thích:
Chữ ký dùng cho nạp chồng là tên + tham số. Kiểu trả về không được tính. Trình biên dịch sẽ không thể hiểu bạn muốn gọi phương thức nào.

3. Thực tiễn tốt nhất

Để tránh vấp phải rắc rối với kế thừa và nạp chồng, hãy tuân theo các khuyến nghị sau:

Luôn dùng chú thích @Override cho các phương thức được ghi đè

Điều này không chỉ tăng tính dễ đọc mà còn giúp tránh lỗi chữ ký. Nếu bạn vô tình thay đổi tham số hoặc tên phương thức, trình biên dịch sẽ báo ngay.

@Override
public void makeSound() {
    System.out.println("Bark!");
}

Phân biệt rõ nạp chồng và ghi đè

  • Ghi đè (override): thay đổi hành vi của phương thức lớp cha — chữ ký phải trùng khớp.
  • Nạp chồng (overload): thêm phương thức mới cùng tên nhưng khác tham số.

Bảng minh hoạ:

Nạp chồng (overloading) Ghi đè (overriding)
Ở đâu Trong cùng một lớp/hệ phân cấp Trong lớp con
Tên phương thức Giống nhau Giống nhau
Tham số Khác nhau Trùng khớp
Kiểu trả về Có thể khác Phải trùng/tương thích
Chú thích Không bắt buộc @Override được khuyến nghị

Đừng lạm dụng nạp chồng

Nếu một phương thức có quá nhiều phiên bản nạp chồng, mã sẽ trở nên khó đọc và rối rắm. Tốt hơn là dùng đối tượng tham số hoặc mẫu thiết kế Builder nếu có quá nhiều biến thể.

4. Ví dụ: hệ thống quản lý vật nuôi

Giả sử bạn xây dựng một hệ thống quản lý vật nuôi đơn giản. Bạn có lớp cơ sở Pet và các lớp con CatDog.

public class Pet {
    private String name;
    public Pet(String name) {
        this.name = name;
    }
    public void speak() {
        System.out.println(name + " phát ra âm thanh khó hiểu.");
    }
}

public class Cat extends Pet {
    public Cat(String name) {
        super(name);
    }
    @Override
    public void speak() {
        System.out.println(getName() + " nói: Meo!");
    }
    // Lỗi: Pet chưa có phương thức getName()!
}

Lỗi điển hình:
Khi cố ghi đè phương thức, ta lại gọi một phương thức không tồn tại trong lớp cơ sở. Tốt hơn nên thêm getter:

public class Pet {
    private String name;
    public Pet(String name) { this.name = name; }
    public String getName() { return name; }
    public void speak() { System.out.println(name + " phát ra âm thanh khó hiểu."); }
}

Giờ mọi thứ hoạt động đúng và ta có thể dùng tính đa hình:

Pet myPet = new Cat("Barsik");
myPet.speak(); // Barsik nói: Meo!

5. Những lỗi phổ biến với kế thừa và nạp chồng

Lỗi số 1: quên gọi super(...).
Nếu lớp cơ sở có logic quan trọng trong constructor nhưng bạn không gọi, chương trình có thể hành xử bất thường, thậm chí không biên dịch được.

Lỗi số 2: ghi đè nhầm phương thức.
Muốn thay đổi hành vi của phương thức lớp cha nhưng thực tế lại thêm phương thức mới có tên hoặc tham số giống giống. Kết quả: phương thức cũ vẫn chạy như trước, còn phương thức mới — chẳng ai gọi.

Lỗi số 3: cố ghi đè phương thức final.
Java sẽ không cho phép — và đó là điều tốt! Nhưng nếu thấy lỗi biên dịch, hãy tìm final.

Lỗi số 4: nạp chồng quá mức.
Khi bạn có 10 biến thể của phương thức calculate và chính bạn cũng nhầm lẫn không biết cái nào được gọi — đã đến lúc dừng lại và nghĩ về refactor.

Lỗi số 5: vi phạm nguyên lý Liskov.
Nếu lớp con làm thay đổi ý nghĩa hành vi của lớp cơ sở, toàn bộ kiến trúc có thể “sụp đổ”. Ví dụ, nếu bạn có lớp Shape với phương thức getArea(), còn lớp con BrokenShape trả về -1, điều đó có thể dẫn đến những bug kỳ quặc.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION