CodeGym /Các khóa học /JAVA 25 SELF /Ghi đè phương thức (overriding): khác với nạp chồng

Ghi đè phương thức (overriding): khác với nạp chồng

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

1. Ghi đè phương thức

Ghi đè (overriding) — là khả năng trong lớp con viết phiên bản riêng của một phương thức đã tồn tại trong lớp cha. Nhờ vậy, tính đa hình thực sự hoạt động trong thời gian chạy chương trình (run-time).

Nói đơn giản:
Nếu bạn có một lớp cơ sở với một phương thức, và bạn muốn lớp con thực thi phương thức đó theo cách riêng, bạn chỉ cần khai báo một phương thức có cùng chữ ký trong lớp con. Khi gọi phương thức qua tham chiếu kiểu cơ sở, phiên bản phương thức của kiểu thực tế của đối tượng sẽ được gọi.

Ví dụ thực tế.
Hãy tưởng tượng bạn có một “đội” động vật và bạn yêu cầu mỗi con “phát ra âm thanh”. Với tất cả, đó là lệnh makeSound(), nhưng chó sủa, mèo kêu meo, còn bò thì rống. Trong mã, trông như gọi cùng một phương thức, nhưng kết quả khác nhau — đó chính là “ma thuật” của ghi đè!

Cú pháp ghi đè

Ví dụ cơ bản

class Animal {
    void makeSound() {
        System.out.println("Động vật phát ra một âm thanh nào đó");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Gâu!");
    }
}

class Cat extends Animal {
    @Override
    void makeSound() {
        System.out.println("Meo!");
    }
}

Ở lớp Animal có định nghĩa phương thức makeSound(). Các lớp con DogCat ghi đè phương thức này, cung cấp hiện thực riêng.

Chú thích @Override

Trong Java, khuyến nghị (và là thói quen tốt) đánh dấu các phương thức được ghi đè bằng chú thích @Override:

@Override
void makeSound() { ... }

Điều này không bắt buộc để mã chạy, nhưng:

  • Trình biên dịch sẽ kiểm tra xem bạn có thực sự ghi đè phương thức (chứ không phải vô tình gõ sai tên hoặc tham số) hay không.
  • Cải thiện khả năng đọc mã — lập trình viên khác sẽ thấy ngay đây là phương thức ghi đè phương thức của lớp cha.

Gọi phương thức đã ghi đè

Animal myDog = new Dog();
myDog.makeSound(); // Sẽ in: Gâu!

Dù biến có kiểu Animal, thực tế nó trỏ đến đối tượng Dog, và phương thức của lớp Dog được gọi. Đây chính là liên kết động (late binding).

2. Sự khác nhau giữa ghi đè (overriding) và nạp chồng (overloading)

Nạp chồng (overloading)

  • Trong cùng một lớp (hoặc trong hệ phân cấp, nhưng luôn “cạnh nhau”).
  • Các phương thức có cùng tên nhưng tham số khác nhau (kiểu, số lượng, thứ tự).
  • Việc chọn phương thức diễn ra ở thời điểm biên dịch.
void print(int x) { ... }
void print(String s) { ... }

Ghi đè (overriding)

  • Ở các lớp khác nhau: phương thức được khai báo trong lớp cha và ghi đè trong lớp con.
  • Các phương thức có cùng tên và cùng chữ ký (tham số và kiểu trả về).
  • Việc chọn phương thức diễn ra lúc chạy (run-time), dựa trên kiểu thực tế của đối tượng.

Bảng so sánh

Nạp chồng (overloading) Ghi đè (overriding)
Ở đâu Trong cùng một lớp Trong lớp cha và lớp con
Tên phương thức Giống nhau Giống nhau
Tham số Khác nhau Giống hệt
Kiểu trả về Có thể khác Phải trùng hoặc là kiểu con
Khi được quyết định Thời điểm biên dịch Thời điểm chạy (run-time)
Annotation Không bắt buộc @Override (khuyến nghị)

3. Các quy tắc khi ghi đè phương thức

Ghi đè rất mạnh mẽ, nhưng đi kèm những quy tắc nghiêm ngặt. Hãy đi lần lượt.

Chữ ký phương thức phải trùng khớp

  • Tên phương thức, kiểu và thứ tự các tham số phải giống hệt phương thức trong lớp cha.
  • Kiểu trả về phải trùng hoặc đồng biến (covariant, tức là kiểu con của kiểu trả về của phương thức cha).

Ví dụ về kiểu trả về đồng biến:

class Animal {
    Animal reproduce() { return new Animal(); }
}
class Cat extends Animal {
    @Override
    Cat reproduce() { return new Cat(); } // OK! Cat là kiểu con của Animal
}

Bộ sửa truy cập

Bộ sửa truy cập của phương thức đã ghi đè không được chặt hơn so với phương thức trong lớp cha. Nếu phương thức trong lớp cha là public, thì trong lớp con nó bắt buộc vẫn phải là public. Không thể làm cho nó kém truy cập hơn (protected hoặc private).

Ví dụ:

class Parent {
    public void greet() { }
}
class Child extends Parent {
    // void greet() { } // Lỗi! Mặc định là package-private, hạn chế hơn public
    @Override
    public void greet() { } // OK
}

Ngoại lệ

  • Phương thức đã ghi đè không được ném thêm checked exception mới mà không được khai báo ở phương thức cơ sở.
  • Có thể ném ít hơn hoặc cùng các ngoại lệ.

Ví dụ:

class Parent {
    void doWork() throws IOException { }
}
class Child extends Parent {
    @Override
    void doWork() throws FileNotFoundException { } // OK, FileNotFoundException là kiểu con của IOException
    // void doWork() throws SQLException { } // Lỗi! SQLException không được khai báo ở lớp cha
}

Phương thức static không thể được ghi đè

Các phương thức static có thể bị “che” (hidden), nhưng không thể ghi đè. Nếu bạn khai báo một phương thức static có cùng chữ ký trong lớp con, đó không phải là ghi đè! Chỉ là che khuất phương thức, chứ không phải đa hình.

class Animal {
    static void info() { System.out.println("Animal"); }
}
class Dog extends Animal {
    static void info() { System.out.println("Dog"); }
}

Lệnh gọi Dog.info() sẽ in "Dog", nhưng nếu gọi qua biến kiểu Animal, sẽ gọi phương thức Animal.info(). Đó không phải là đa hình!

Không thể ghi đè phương thức final

Nếu phương thức trong lớp cha được khai báo là final, cố gắng ghi đè nó sẽ dẫn tới lỗi biên dịch.

class Animal {
    final void sleep() { }
}
class Dog extends Animal {
    // @Override
    // void sleep() { } // Lỗi! Không thể ghi đè phương thức final
}

4. Ví dụ thực hành

Hãy xem thực tế ghi đè hoạt động thế nào và nó khác gì với nạp chồng.

Ví dụ 1: Lớp Shape và các lớp con

class Shape {
    void draw() {
        System.out.println("Vẽ hình");
    }
}

class Circle extends Shape {
    @Override
    void draw() {
        System.out.println("Vẽ hình tròn");
    }
}

class Rectangle extends Shape {
    @Override
    void draw() {
        System.out.println("Vẽ hình chữ nhật");
    }
}

Sử dụng tính đa hình:

public class Main {
    public static void main(String[] args) {
        Shape s1 = new Circle();
        Shape s2 = new Rectangle();
        s1.draw(); // Vẽ hình tròn
        s2.draw(); // Vẽ hình chữ nhật
    }
}

Dù biến được khai báo là Shape, nhưng phương thức được gọi chính là phương thức của lớp mà đối tượng thực sự thuộc về.

Ví dụ 2: Khác với nạp chồng

class Printer {
    void print(String s) {
        System.out.println("Chuỗi: " + s);
    }

    void print(int n) {
        System.out.println("Số: " + n);
    }
}

Ở đây cả hai phương thức đều tên là print, nhưng tham số khác nhau — đây là nạp chồng, không phải ghi đè.

5. Ghi đè và gọi phương thức của lớp cha (super)

Đôi khi trong phương thức đã ghi đè, bạn muốn chạy logic của lớp cha trước rồi mới thêm phần của mình. Khi đó dùng từ khóa super.

class Animal {
    void makeSound() {
        System.out.println("Động vật phát ra âm thanh");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        super.makeSound(); // Gọi phương thức của lớp cha
        System.out.println("Gâu!");
    }
}

Lệnh gọi new Dog().makeSound() sẽ in:

Động vật phát ra âm thanh
Gâu!

Cách hoạt động của liên kết động (late binding)

Khi bạn gọi phương thức qua tham chiếu kiểu cơ sở, Java trong thời gian chạy sẽ xem tham chiếu đó trỏ đến đối tượng thực tế nào và gọi đúng phiên bản phương thức được định nghĩa trong lớp của đối tượng đó.

Animal a = new Cat();
a.makeSound(); // Sẽ gọi Cat.makeSound(), chứ không phải Animal.makeSound()

Đây là nền tảng của tính đa hình trong Java.

6. Liên quan thế nào tới ứng dụng của bạn

Trong ứng dụng học tập của chúng ta (ví dụ, hệ thống quản lý nhân sự), bạn có thể tạo lớp cơ sở Employee với phương thức work(), còn các lớp con ManagerDeveloper có thể hiện thực phương thức này theo cách riêng:

class Employee {
    void work() {
        System.out.println("Nhân viên đang làm việc");
    }
}

class Manager extends Employee {
    @Override
    void work() {
        System.out.println("Quản lý điều phối");
    }
}

class Developer extends Employee {
    @Override
    void work() {
        System.out.println("Lập trình viên viết mã");
    }
}

Bây giờ bạn có thể lưu tất cả nhân viên trong cùng một mảng hoặc danh sách:

Employee[] employees = {new Manager(), new Developer(), new Developer()};
for (Employee e : employees) {
    e.work(); // Mỗi người sẽ in khác nhau!
}

7. Các lỗi thường gặp khi ghi đè phương thức

Lỗi số 1: sai chính tả ở tên phương thức hoặc tham số. Nếu bạn vô tình sai tên phương thức hoặc tham số, bạn không ghi đè mà tạo phương thức mới. Kết quả là đa hình không hoạt động. Vì vậy luôn dùng chú thích @Override — trình biên dịch sẽ nhắc bạn ngay.

Lỗi số 2: bộ sửa truy cập chặt hơn. Nếu ở lớp cha phương thức là public, còn ở lớp con bạn khai báo là protected hoặc không có bộ sửa nào — bạn sẽ nhận lỗi biên dịch.

Lỗi số 3: cố ghi đè phương thức static hoặc final. Phương thức static không thể ghi đè, còn phương thức final thì hoàn toàn không thể bị ghi đè. Nếu cố làm — trình biên dịch sẽ dừng bạn lại.

Lỗi số 4: đổi kiểu trả về sang kiểu không tương thích. Nếu kiểu trả về ở lớp con không trùng với kiểu ở lớp cha (và không phải kiểu con của nó), trình biên dịch sẽ không cho phép ghi đè.

Lỗi số 5: thêm checked exception mới. Phương thức đã ghi đè không thể ném các checked exception mới mà không có trong khai báo của phương thức cơ sở. Nếu làm vậy — trình biên dịch sẽ báo lỗi.

Lỗi số 6: quên super. Nếu trong phương thức đã ghi đè bạn muốn giữ một phần hành vi của lớp cha, đừng quên gọi rõ ràng super.methodName(). Java sẽ không tự làm điều đó cho bạn.

Bây giờ bạn đã biết ghi đè phương thức hoạt động thế nào, nó khác gì với nạp chồng, và cách nó hiện thực hóa tính đa hình trong Java. Ở bài tiếp theo, chúng ta sẽ xem cách áp dụng đa hình trong thực tế — với collections, mảng và các bài toán thực tế!

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