99 Ngày Java — Ngày 15: Đa hình (Polymorphism)
Ngày 14 gác lại một câu hỏi: NhanVien nv = new LapTrinhVien(...) — biến kiểu cha cầm đối tượng con — thì nv.tinhLuong() chạy bản của ai? Câu trả lời mở ra trụ cột thứ ba, được xem là đẹp nhất của OOP: đa hình (polymorphism) — "nhiều hình hài": một lời gọi, nhiều cách thực thi, quyết định lúc chương trình đang chạy. Đây chính là cơ chế cho phép viết code hôm nay mà đón được cả những class chưa ra đời.

Upcasting & dynamic dispatch — trái tim đa hình
NhanVien nv = new LapTrinhVien("An", 20_000_000, "Java");
// ↑ UPCASTING: biến kiểu CHA cầm đối tượng CON — tự động, luôn an toàn
// (LapTrinhVien LÀ MỘT NhanVien — Ngày 14)
System.out.println(nv.tinhLuong());
// Chạy bản của AI? → bản của LapTrinhVien (lương + phụ cấp)!
// Kiểu BIẾN là NhanVien, nhưng ĐỐI TƯỢNG thật là LapTrinhVien
// → JVM nhìn ĐỐI TƯỢNG THẬT lúc RUNTIME để chọn method
// → tên sang trọng: DYNAMIC DISPATCH
// Sức mạnh thật sự — MỘT mảng, NHIỀU hình hài (Ngày 08 + Ngày 14):
NhanVien[] congTy = {
new LapTrinhVien("An", 20_000_000, "Java"),
new KeToan("Bình", 15_000_000),
new QuanLy("Chi", 30_000_000, 5)
};
double tongQuyLuong = 0;
for (NhanVien n : congTy) {
tongQuyLuong += n.tinhLuong(); // MỖI vòng chạy MỘT bản khác nhau!
}
// Thêm chức danh mới? Thêm class mới — vòng lặp NÀY không sửa một chữTách bạch hai khái niệm là nắm được 80% bài: kiểu của biến (compiler dùng — quyết định được gọi gì) vs kiểu của đối tượng thật (JVM dùng lúc runtime — quyết định chạy bản nào).
Vòng lặp tính quỹ lương là khoảnh khắc mọi mảnh ghép khớp: mảng (Ngày 08) + kế thừa & override (Ngày 14) + dispatch hôm nay = xử lý cả tập thể không cần biết ai là ai.
"Thêm chức danh không sửa vòng lặp" chính là tinh thần Open–Closed — mở để mở rộng, đóng để sửa đổi; đại tiệc SOLID hẹn Ngày 91.
Downcasting, instanceof & pattern matching
Upcasting có giá của nó: qua "cặp kính" kiểu cha, các method riêng của con biến mất. Muốn dùng lại phải downcast — và đây là vùng nguy hiểm cần lá chắn:
NhanVien nv = new LapTrinhVien("An", 20_000_000, "Java");
// Qua "cặp kính" NhanVien, chỉ thấy method CỦA CHA:
// nv.deployThuSau(); ✗ lỗi biên dịch — NhanVien không có method này!
// DOWNCASTING: ép ngược xuống kiểu con — PHẢI tường minh, và CÓ RỦI RO:
LapTrinhVien dev = (LapTrinhVien) nv; // ✓ chạy — đối tượng thật đúng là dev
dev.deployThuSau();
NhanVien nv2 = new KeToan("Bình", 15_000_000);
LapTrinhVien x = (LapTrinhVien) nv2; // ✗ compile OK... nổ RUNTIME:
// ClassCastException: KeToan cannot be cast to LapTrinhVien
// Lá chắn cổ điển:
if (nv2 instanceof LapTrinhVien) {
LapTrinhVien d = (LapTrinhVien) nv2; // kiểm tra rồi mới ép
}
// Java 16+ — PATTERN MATCHING: kiểm tra + ép + đặt tên trong MỘT nhát:
if (nv2 instanceof LapTrinhVien d) {
d.deployThuSau(); // d sẵn sàng, gọn và an toàn
}Ghép chuỗi ép kiểu cho trọn bộ: Ngày 05 cast primitive sai thì mất dữ liệu âm thầm; cast đối tượng sai thì nổ
ClassCastException— ồn ào hơn, nhưng vẫn là lỗi runtime đau.Pattern matching (chuẩn từ Java 16, người quen từ switch Ngày 06) là cú pháp nên dùng mặc định: gọn, không lặp tên kiểu, không kẽ hở giữa kiểm tra và ép.
Nhưng lùi một bước: rải
instanceofkhắp nơi để "hỏi từng đứa là ai" là mùi thiết kế — thường có nghĩa method đó nên là override để dispatch tự lo.instanceoflà lối thoát hiểm, không phải cửa chính.
Lập trình theo hợp đồng
// Đa hình biến THAM SỐ và KIỂU TRẢ VỀ thành hợp đồng rộng:
static void inPhieuLuong(NhanVien nv) { // nhận MỌI loại nhân viên —
System.out.println(nv.gioiThieu()); // hiện tại VÀ tương lai
System.out.println("Lương: " + nv.tinhLuong());
}
inPhieuLuong(new LapTrinhVien(...)); // ✓
inPhieuLuong(new KeToan(...)); // ✓ — không cần overload gì thêm!
// Đây chính là nguyên lý bạn ĐÃ hưởng ở series Spring:
// StorageService s = context.getBean(StorageService.class);
// → nhận S3StorageService hay LocalStorageService tùy profile (Spring Ngày 09)
// → code dùng KHÔNG CẦN BIẾT — "lập trình theo hợp đồng, không theo cài đặt"
// Lưu ý cuối: đa hình áp dụng cho METHOD của đối tượng.
// FIELD không đa hình (nhìn theo kiểu BIẾN), method static cũng không —
// hai góc khuất hay ra câu hỏi mẹo phỏng vấn!Tham số kiểu cha là cánh cổng rộng: inPhieuLuong viết một lần, phục vụ mọi loại nhân viên — kể cả loại chưa được viết ra. Và bí mật lớn nhất: đây chính là nền móng của Dependency Injection bên series Spring — container tiêm đối tượng thật nào vào biến kiểu hợp đồng là chuyện của cấu hình; code nghiệp vụ chỉ biết hợp đồng. Hai series hôm nay chính thức chạm vào nhau.
toString — đa hình bạn dùng mỗi ngày
public class NhanVien {
@Override
public String toString() { // override từ Object (Ngày 14)
return "NhanVien{ten='%s', luong=%.0f}".formatted(ten, tinhLuong());
}
}
NhanVien[] congTy = { new LapTrinhVien("An", 20_000_000, "Java"), ... };
System.out.println(Arrays.toString(congTy));
// TRƯỚC khi override: [NhanVien@1b6d3586, ...] — vô nghĩa
// SAU khi override: [NhanVien{ten='An', luong=23000000}, ...]
//
// println gọi toString QUA ĐA HÌNH — viết từ 30 năm trước
// mà vẫn chạy đúng bản của class bạn viết HÔM NAY.
// Đa hình chính là cách code CŨ gọi được code MỚI.printlncủa Ngày 01 hóa ra vẫn giấu nghề: nó gọitoString()qua đa hình — override là mọi chỗ in tự đẹp lên, không sửa dòng in nào.Cặp
equals/hashCodecũng chờ được override đúng cách như thế — hợp đồng đầy đủ hẹn Ngày 24.
Bài tập nhỏ
Hoàn thiện mảng
NhanVien[]ba loại — in phiếu lương cả công ty bằng một vòng lặp; thêm loại thứ tưThucTapSinhvà xác nhận vòng lặp không đổi.Cố tình ép
KeToanthànhLapTrinhVien— đọcClassCastException, rồi che chắn bằng pattern matching.Override
toStringcho cả cây lớp — inArrays.toString(congTy)trước/sau để thấy khác biệt.Câu hỏi mẹo tự kiểm: cha con cùng có field
chucDanh—nv.chucDanhlấy của ai? Viết code kiểm chứng và giải thích (gợi ý: field không đa hình!).
Kết luận
Trụ cột thứ ba — và là trái tim của OOP — đã đập: upcasting mở cổng rộng, dynamic dispatch chọn đúng bản theo đối tượng thật lúc runtime, downcasting + pattern matching là lối thoát hiểm có lá chắn, và lập trình theo hợp đồng nối thẳng sang thế giới Spring. Ba trụ cột đã dựng — còn một mảnh: làm sao ép mọi lớp con phải override một method mà cha không thể tự cài đặt? Ngày 16: abstract class — lớp "khuyết" cố ý, khuôn mẫu bắt buộc của cả gia tộc. Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


