Backend

99 Ngày Java — Ngày 15: Đa hình (Polymorphism)

SSite Admin
11 tháng 08, 2026 6 phút đọc 1 lượt xem
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.

Sketchnote Ngày 15: đa hình trong Java — upcasting, dynamic dispatch, downcasting, instanceof và pattern matching

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 instanceof khắ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. instanceof là 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.

Bộ dụng cụ vẽ đa sắc — cùng một lời gọi, mỗi đối tượng thể hiện một hình hài riêng

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.
  • println của Ngày 01 hóa ra vẫn giấu nghề: nó gọi toString() 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/hashCode cũ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ư ThucTapSinh và xác nhận vòng lặp không đổi.

  • Cố tình ép KeToan thành LapTrinhVien — đọc ClassCastException, rồi che chắn bằng pattern matching.

  • Override toString cho cả cây lớp — in Arrays.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 chucDanhnv.chucDanh lấ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!

S

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.

Bài viết liên quan

99 Ngày Spring — Ngày 15: ResponseEntity & status code

Ngày 15 của 99 Ngày Spring: bảng status code tối thiểu (200/201/204/400/404/409/500) với ranh giới 4xx vs 5xx, ResponseEntity điều khiển status–header–body, nghi thức 201 Created + Location cho POST, 204 cho DELETE, và nguyên tắc "lỗi thì ném, đừng tự build".

11 thg 8, 20265 phút0
99 Ngày Spring — Ngày 14: Bean Validation — @Valid & bộ constraint chuẩn

Ngày 14 của 99 Ngày Spring: Bean Validation khai báo luật ngay trên DTO — @Valid kích hoạt, bộ constraint chuẩn (@NotBlank vs @NotEmpty vs @NotNull, @Size, @Email, @Min/@Max), bẫy int vs Integer, validate object lồng nhau không tự lan, và lỗi 400 gom một lượt.

10 thg 8, 20265 phút25
99 Ngày Java — Ngày 14: Kế thừa (Inheritance)

Ngày 14 của 99 Ngày Java: extends từ class cha, override với @Override và super., thứ tự khởi tạo từ cha trước con sau qua super(...), protected .v.v

10 thg 8, 20266 phút28