1. Ghi đè phương thức
Trong đời sống, ta thường gặp những tình huống khi “lớp con” cư xử theo cách riêng. Ví dụ, mọi loài động vật đều biết phát ra âm thanh, nhưng mèo — “meo”, chó — “gâu”, còn lập trình viên — “ôi, lại bug nữa!”. Trong lập trình, điều này được hiện thực thông qua ghi đè phương thức (override).
Ghi đè phương thức là khi lớp con cung cấp hiện thực của riêng mình cho một phương thức đã được khai báo trong lớp cha. Tức là “thay thế” hành vi mặc định bằng hành vi đặc thù của nó.
Tương tự. Nếu coi lớp cha như công thức nấu borscht “chuẩn hãng”, thì ghi đè phương thức là khi bà thêm vào đó nguyên liệu bí mật của riêng mình. Borscht vẫn là borscht, nhưng mỗi người một vị.
Để ghi đè một phương thức, bạn cần khai báo trong lớp con một phương thức có chữ ký y hệt (tên, tham số, kiểu trả về) như ở lớp cha.
Ví dụ: động vật và âm thanh của chúng
class Animal {
void makeSound() {
System.out.println("Some generic animal sound");
}
}
class Dog extends Animal {
// Ghi đè phương thức makeSound()
void makeSound() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
// Ghi đè phương thức makeSound()
void makeSound() {
System.out.println("Meow!");
}
}
Bây giờ, nếu tạo đối tượng Dog và gọi makeSound(), bạn sẽ nghe "Woof!", chứ không phải "Some generic animal sound".
Minh họa trong mã
public class Main {
public static void main(String[] args) {
Animal generic = new Animal();
Dog dog = new Dog();
Cat cat = new Cat();
generic.makeSound(); // Some generic animal sound
dog.makeSound(); // Woof!
cat.makeSound(); // Meow!
}
}
Quan trọng: nếu lớp con không có phương thức với cùng chữ ký, phương thức của lớp cha sẽ được sử dụng.
2. Annotation @Override: để làm gì và dùng thế nào
Trong Java, thông lệ là đánh dấu các phương thức đã ghi đè bằng annotation @Override. Đây không chỉ là “trang trí” cho mã, mà còn là công cụ hữu ích:
- Trình biên dịch kiểm tra xem bạn có thực sự đang ghi đè phương thức của lớp cha hay không. Nếu bạn sai tên, kiểu tham số hoặc kiểu trả về — trình biên dịch sẽ báo lỗi.
- Tăng khả năng đọc mã. Lập trình viên khác sẽ thấy ngay: “Ồ, phương thức này ghi đè từ lớp cha”.
Ví dụ với @Override
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof!");
}
}
Nếu vô tình viết void makeSond() (gõ sai!) cho phương thức được đánh dấu @Override, trình biên dịch sẽ báo lỗi: "Method does not override or implement a method from a supertype".
Chuẩn hiện đại. Sử dụng @Override là thực hành tốt và là chuẩn trong ngành. Dù trình biên dịch không bắt buộc, hãy luôn đặt annotation này — điều đó sẽ giúp cả bạn lẫn đồng nghiệp.
3. Cách lời gọi phương thức đã ghi đè hoạt động
Khi bạn gọi phương thức trên một đối tượng của lớp con, hiện thực trong lớp con sẽ được sử dụng, ngay cả khi biến được khai báo theo kiểu lớp cha.
Ví dụ: đa hình trong thực tế
Animal animal = new Dog();
animal.makeSound(); // "Woof!", chứ không phải "Some generic animal sound"
Ở đây biến có kiểu Animal, nhưng thực chất nó đang giữ đối tượng Dog. Java “hiểu” rằng cần gọi phương thức đã ghi đè trong Dog. Đây chính là đa hình (chi tiết — ở các bài giảng tiếp theo).
4. Hạn chế và quy tắc khi ghi đè
Chữ ký phương thức
- Tên, kiểu và thứ tự tham số phải trùng với phương thức trong lớp cha.
- Kiểu trả về phải trùng hoặc đồng biến (là kiểu con của kiểu trả về ở lớp cha). Ví dụ, nếu lớp cha trả về Animal, còn lớp con — Dog, điều này được cho phép.
Bộ sửa truy cập
- Không được làm cho mức truy cập hẹp hơn so với lớp cha.
- Nếu phương thức ở lớp cha là public, thì phương thức ghi đè cũng phải là public.
- Nếu ở lớp cha là protected, thì phương thức ghi đè có thể là protected hoặc public.
Nếu cố làm ngược lại, trình biên dịch sẽ báo: "Cannot reduce the visibility of the inherited method".
Ngoại lệ
- Phương thức đã ghi đè không được ném thêm ngoại lệ có kiểm tra (checked) mới mà không có trong khai báo của lớp cha.
- Có thể ném ít ngoại lệ hơn lớp cha, hoặc ném các kiểu con của chúng.
static, final, private
- Không thể ghi đè các phương thức được khai báo là static hoặc final, cũng như các phương thức riêng tư (private).
- static — là che khuất (hiding), không phải ghi đè.
- final — hoàn toàn không thể ghi đè, Java bảo vệ các phương thức như vậy.
- private — không nhìn thấy trong lớp con, không thể ghi đè (chỉ có thể khai báo một phương thức mới cùng tên).
Constructor
Constructor không được kế thừa và cũng không được ghi đè. Mỗi lớp có các constructor riêng của nó.
5. Phát triển ứng dụng học tập “Sở thú”
Đến lúc áp dụng lý thuyết vào thực hành! Hãy tiếp tục phát triển ứng dụng “sở thú” của chúng ta.
Bước 1. Lớp cơ sở Animal
public class Animal {
public void makeSound() {
System.out.println("Some generic animal sound");
}
public void sleep() {
System.out.println("Zzz...");
}
}
Bước 2. Các lớp con Dog và Cat
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Woof!");
}
// Phương thức bổ sung chỉ dành cho Dog
public void fetch() {
System.out.println("Dog brings the stick!");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow!");
}
// Phương thức bổ sung chỉ dành cho Cat
public void scratch() {
System.out.println("Cat scratches the sofa!");
}
}
Bước 3. Sử dụng ghi đè
public class ZooTest {
public static void main(String[] args) {
Animal generic = new Animal();
Animal dog = new Dog();
Animal cat = new Cat();
generic.makeSound(); // Some generic animal sound
dog.makeSound(); // Woof!
cat.makeSound(); // Meow!
// dog.fetch(); // Lỗi! Biến kiểu Animal không biết fetch()
// cat.scratch(); // Tương tự
// Nhưng nếu chỉ định kiểu rõ ràng:
if (dog instanceof Dog) {
((Dog) dog).fetch(); // Dog brings the stick!
}
if (cat instanceof Cat) {
((Cat) cat).scratch(); // Cat scratches the sofa!
}
}
}
Ghi chú:
Phương thức makeSound() hoạt động theo cơ chế đa hình — phiên bản từ lớp thực sự của đối tượng sẽ được gọi. Còn các phương thức đặc thù (fetch, scratch) chỉ truy cập được khi ép kiểu tường minh — điều này quan trọng để hiểu cách kế thừa và ghi đè hoạt động.
6. Ví dụ với kiểu trả về (tính đồng biến)
Đôi khi bạn muốn phương thức đã ghi đè trả về kiểu “hẹp” hơn. Ví dụ:
class Animal {
Animal getFriend() {
return new Animal();
}
}
class Dog extends Animal {
@Override
Dog getFriend() { // Kiểu trả về — Dog, là kiểu con của Animal
return new Dog();
}
}
Đây được gọi là tính đồng biến của kiểu trả về và được phép trong Java (bắt đầu từ Java 5).
7. Điều gì xảy ra nếu không dùng @Override?
Nếu bạn vô tình sai tên phương thức hoặc tham số, Java sẽ không báo lỗi khi không có annotation @Override. Kết quả là bạn không ghi đè, mà tạo ra một phương thức mới, và hành vi mong đợi sẽ không thay đổi.
Ví dụ lỗi
class Dog extends Animal {
// Gõ nhầm: makeSoud thay vì makeSound
void makeSoud() {
System.out.println("Woof!");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog();
dog.makeSound(); // Sẽ in "Some generic animal sound"
}
}
Nếu có @Override, trình biên dịch sẽ báo lỗi: "Method does not override or implement a method from a supertype".
8. Các lỗi thường gặp khi ghi đè phương thức
Lỗi số 1: thiếu annotation @Override.
Không có nó rất dễ sai tên phương thức hoặc tham số. Kết quả là phương thức sẽ không được ghi đè và chương trình hoạt động không như mong đợi.
Lỗi số 2: cố thu hẹp mức truy cập.
Nếu phương thức ở lớp cha là public, còn bạn viết protected hoặc private — bạn sẽ nhận lỗi biên dịch.
Lỗi số 3: chữ ký không khớp.
Nếu tham số khác dù chỉ về kiểu — đó không còn là ghi đè, mà là nạp chồng (overloading).
Lỗi số 4: cố ghi đè phương thức final hoặc static.
Java sẽ không cho phép: final bảo vệ khỏi ghi đè, còn các phương thức static thì không thể ghi đè (chỉ có thể che khuất).
Lỗi số 5: đổi kiểu trả về sang kiểu không tương thích.
Chỉ có thể trả về kiểu con của kiểu trả về ở lớp cha (đồng biến), chứ không phải một kiểu hoàn toàn khác.
GO TO FULL VERSION