1. Đa hình trong collection: để làm gì?
Hãy bắt đầu với câu hỏi: “Vì sao cần đa hình trong các chương trình thực tế?”
Hãy tưởng tượng một sở thú. Bạn có lớp cơ sở Animal, cùng hàng loạt lớp con: Dog, Cat, Cow, Parrot và cả Platypus (thú mỏ vịt, dành cho người thích sự độc lạ). Mỗi loài đều biết phát ra âm thanh (makeSound()), nhưng theo cách riêng của mình.
Thay vì tạo mảng riêng cho từng loài, bạn khai báo một mảng hoặc danh sách kiểu Animal và cho vào đó bất kỳ ai:
Animal[] animals = {
new Dog(),
new Cat(),
new Cow(),
new Parrot()
};
Bây giờ bạn có thể duyệt qua mảng này và gọi makeSound() cho từng phần tử:
for (Animal animal : animals) {
animal.makeSound();
}
Phép màu! Mỗi đối tượng tự biết phát ra âm thanh nào, và bạn không cần viết một đống if hay switch.
Ví dụ minh họa
Giống như bạn ra lệnh “Lên tiếng!” cho một nhóm động vật, và mỗi con sẽ tự quyết định phải làm gì: chó thì sủa, mèo kêu meo meo, còn bò thì rống. Bạn không cần chỉ rõ con nào là con nào — chỉ việc gọi cùng một phương thức.
2. Ví dụ thực tế: hệ phân cấp nhân viên
Hãy làm ví dụ gần gũi hơn (và sát với công việc tương lai trong IT). Giả sử có một công ty với nhiều loại nhân viên: quản lý, lập trình viên, kiểm thử viên. Mỗi người đều có phương thức work(), nhưng thực thi khác nhau.
Khai báo lớp cơ sở
public class Employee {
public void work() {
System.out.println("Nhân viên đang làm việc...");
}
}
Các lớp con
public class Manager extends Employee {
@Override
public void work() {
System.out.println("Quản lý đang tổ chức cuộc họp.");
}
}
public class Developer extends Employee {
@Override
public void work() {
System.out.println("Lập trình viên viết mã.");
}
}
public class Tester extends Employee {
@Override
public void work() {
System.out.println("Kiểm thử viên tìm lỗi.");
}
}
Sử dụng mảng/danh sách của kiểu cơ sở
public class CompanyDemo {
public static void main(String[] args) {
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Developer()
};
for (Employee e : team) {
e.work(); // Sẽ gọi phiên bản "đúng" của phương thức cho từng đối tượng
}
}
}
Kết quả chạy:
Quản lý đang tổ chức cuộc họp.
Lập trình viên viết mã.
Kiểm thử viên tìm lỗi.
Lập trình viên viết mã.
Ưu điểm là gì?
- Bạn không phải viết hàng tá kiểm tra kiểu “Nếu là Developer — làm X” nữa.
- Thêm nhân viên mới (ví dụ Designer) — chỉ cần tạo lớp mới và thêm vào mảng.
- Mã sử dụng mảng nhân viên hoàn toàn không đổi!
3. Ưu điểm của đa hình: linh hoạt và mở rộng
Giả sử công ty bạn xuất hiện một loại nhân viên mới — Designer. Tất cả những gì cần làm là tạo một lớp mới:
public class Designer extends Employee {
@Override
public void work() {
System.out.println("Nhà thiết kế vẽ bản mẫu.");
}
}
Bây giờ có thể thêm nhà thiết kế vào đội:
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Designer()
};
Vậy là xong! Chương trình lập tức làm việc đúng với kiểu nhân viên mới mà không thay đổi một dòng nào trong đoạn mã duyệt mảng và gọi work().
Đó chính là khả năng mở rộng: mã của bạn dễ dàng thích ứng với các kiểu đối tượng mới.
4. Hạn chế của đa hình: mặt trái
Tiếc là “phép màu” nào cũng có giới hạn (và có giá, như trong bất kỳ RPG nào).
Chỉ gọi được các phương thức của lớp cơ sở
Khi làm việc với biến kiểu Employee, bạn chỉ có thể gọi các phương thức được khai báo trong lớp Employee. Nếu trong lớp Developer có phương thức đặc thù writeCode(), bạn không thể gọi trực tiếp nó:
Employee e = new Developer();
// e.writeCode(); // Lỗi biên dịch: không có phương thức như vậy trong Employee!
Nếu rất muốn gọi phương thức đặc thù, bạn sẽ phải ép kiểu. Nhưng đó là phương án cuối cùng. Nếu bạn thường xuyên ép kiểu, có lẽ nên xem lại thiết kế — lớp cơ sở hoặc interface phải chứa phương thức cần thiết.
if (e instanceof Developer) {
Developer dev = (Developer) e;
dev.writeCode();
}
Nhưng khi đó bạn đánh mất tính phổ quát và sự tao nhã, vốn là mục tiêu của cách tiếp cận này. Vì vậy, hãy thiết kế lớp cơ sở sao cho chỉ chứa những phương thức thực sự cần cho mọi lớp con.
5. Thực hành: hiện thực hệ phân cấp nhân viên
Kết hợp hữu ích với thú vị: viết một ứng dụng đơn giản có vài kiểu nhân viên và dùng đa hình để xử lý họ.
Bước 1: Lớp cơ sở và các lớp con
// Employee.java
public class Employee {
public void work() {
System.out.println("Nhân viên đang làm việc...");
}
}
// Manager.java
public class Manager extends Employee {
@Override
public void work() {
System.out.println("Quản lý đang tổ chức cuộc họp.");
}
}
// Developer.java
public class Developer extends Employee {
@Override
public void work() {
System.out.println("Lập trình viên viết mã.");
}
}
// Tester.java
public class Tester extends Employee {
@Override
public void work() {
System.out.println("Kiểm thử viên tìm lỗi.");
}
}
Bước 2: Lớp chính
// CompanyDemo.java
public class CompanyDemo {
public static void main(String[] args) {
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Developer()
};
for (Employee e : team) {
e.work();
}
}
}
Bước 3: Thêm khả năng mở rộng
Giả sử sau một tháng, công ty có nhân viên mới — nhà thiết kế. Tất cả những gì cần làm:
public class Designer extends Employee {
@Override
public void work() {
System.out.println("Nhà thiết kế vẽ bản mẫu.");
}
}
Xong, bây giờ có thể thêm nhà thiết kế vào đội:
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Designer()
};
Kết luận
Toàn bộ mã chính (CompanyDemo) vẫn không đổi! Đó là sức mạnh của đa hình.
6. Những sai lầm thường gặp khi dùng đa hình
Lỗi số 1: Kỳ vọng gọi được phương thức đặc thù qua tham chiếu kiểu cơ sở.
Rất nhiều người mới học cố gọi các phương thức đặc thù của lớp con qua biến thuộc kiểu lớp cha. Ví dụ:
Employee e = new Developer();
// e.writeCode(); // Lỗi! Phương thức đó không được định nghĩa trong Employee.
Muốn gọi phương thức đặc thù, bạn phải ép kiểu, nhưng khi đó sẽ mất tính phổ quát.
Lỗi số 2: Không dùng annotation @Override.
Nếu quên annotation này, bạn có thể vô tình viết một phương thức mới thay vì ghi đè (ví dụ gõ sai tên). Khi đó đa hình sẽ không hoạt động, và phiên bản của lớp cha sẽ được gọi.
Lỗi số 3: Thiếu giao diện chung.
Nếu lớp cơ sở không chứa phương thức cần thiết, đa hình là không thể. Ví dụ, nếu trong Employee không có work(), vòng lặp qua mảng nhân viên sẽ không thể gọi phương thức này cho tất cả.
Lỗi số 4: Vi phạm nguyên tắc mở/đóng.
Nếu để thêm một loại nhân viên mới mà phải sửa mã duyệt mảng/danh sách — nghĩa là bạn chưa dùng đa hình đúng cách.
GO TO FULL VERSION