PART 3: THỪA KẾ (INHERITANCE) & ĐA HÌNH (POLYMORPHISM) (LAB 05)
Trong phần này, thầy sẽ cùng bạn tìm hiểu hai bài toán điển hình trong Lab 05:
Bài 5A: Quản lý điểm các loại Học phần của Sinh viên (Lớp trừu tượng Abstract class và Liên kết muộn).
Bài 5B: Quản lý Thùng thư & Bưu phẩm (Thiết kế kế thừa đa hình ứng dụng thực tế).
🏆 1. KHÁI NIỆM CỐT LÕI NGUYÊN LÝ THỪA KẾ & ĐA HÌNH
Trước khi viết code, thầy giải thích nhanh 2 khái niệm này bằng ví dụ đời thực:
Kế thừa (Inheritance): Giống như Xe là lớp cha. Xe máy và Xe ô tô là các lớp con. Xe máy kế thừa tất cả thuộc tính chung của Xe (bánh xe, động cơ), đồng thời bổ sung các tính năng riêng của Xe máy.
Lớp trừu tượng (Abstract Class): Xe nào cũng có hành động chay(), nhưng "Xe máy chạy" khác với "Ô tô chạy". Lớp cha Xe định nghĩa phương thức chay() dạng trừu tượng (chỉ có tên, chưa có ruột). Các lớp con buộc phải viết lại (Override) ruột của hàm này.
Đa hình (Polymorphism) / Liên kết muộn: Trong danh sách có cả Xe máy và Ô tô. Khi bạn bảo "Tất cả khởi động!", mỗi xe tự biết cách nổ máy theo đúng kiểu của riêng nó mà bạn không cần phải dùng if-else phân loại rườm rà.
📚 BÀI 5A: QUẢN LÝ ĐIỂM HỌC PHẦN (LAB 05 - PHẦN 2.1)
1. Đọc hiểu Sơ đồ UML (Lab 05)
Lớp DiemHP (Abstract Class): Lớp cha trừu tượng.
Thuộc tính # tenHP, # soTinChi (Ký hiệu # là protected - cho phép các lớp con truy cập trực tiếp).
Phương thức public abstract double tinhDiem(); (Không có phần thân hàm).
Lớp HPLyThuyet (kế thừa DiemHP):
Thuộc tính riêng: chuyenCan, giuaKy, cuoiKy (số nguyên 0-10).
Công thức tính điểm: chuyenCan×0.1+giuaKy×0.2+cuoiKy×0.7.
Lớp HPThucHanh (kế thừa DiemHP):
Thuộc tính riêng: db1, db2, db3 (điểm 3 bài thực hành số thực 0-10).
Công thức tính điểm: (db1 + db2 + db3) / 3.
Lớp SinhVien: Chứa 2 thuộc tính hp1, hp2 có kiểu dữ liệu là DiemHP (Lớp cha). Đây chính là Đa hình: hp1 hay hp2 có thể lưu một đối tượng HPLyThuyet hoặc HPThucHanh tùy ý.
2. Phân tích & Cài đặt Mã nguồn Java Chi Tiết
Bước 2.1: Lớp trừu tượng DiemHP.java
// Từ khóa abstract báo hiệu đây là Lớp Trừu Tượng (Không thể khởi tạo new DiemHP trực tiếp)
public abstract class DiemHP {
protected String tenHP;
protected int soTinChi;
public DiemHP(String tenHP, int soTinChi) {
this.tenHP = tenHP;
this.soTinChi = soTinChi;
}
public String layTenHP() { return tenHP; }
public int laySoTinChi() { return soTinChi; }
// Phương thức trừu tượng: Lớp con BẮT BUỘC phải cài đặt ruột cho hàm này
public abstract double tinhDiem();
}
Bước 2.2: Lớp con HPLyThuyet.java
public class HPLyThuyet extends DiemHP {
private int chuyenCan;
private int giuaKy;
private int cuoiKy;
// Constructor kế thừa từ lớp cha bằng từ khóa super(...)
public HPLyThuyet(String tenHP, int soTinChi, int chuyenCan, int giuaKy, int cuoiKy) {
super(tenHP, soTinChi); // Gọi constructor của DiemHP
this.chuyenCan = chuyenCan;
this.giuaKy = giuaKy;
this.cuoiKy = cuoiKy;
}
// Ghi đè phương thức tinhDiem() theo công thức riêng của học phần Lý thuyết
@Override
public double tinhDiem() {
return chuyenCan * 0.1 + giuaKy * 0.2 + cuoiKy * 0.7;
}
}
Bước 2.3: Lớp con HPThucHanh.java
public class HPThucHanh extends DiemHP {
private double db1, db2, db3;
public HPThucHanh(String tenHP, int soTinChi, double db1, double db2, double db3) {
super(tenHP, soTinChi);
this.db1 = db1;
this.db2 = db2;
this.db3 = db3;
}
// Ghi đè phương thức tinhDiem() tính TB 3 bài thực hành
@Override
public double tinhDiem() {
return (db1 + db2 + db3) / 3.0;
}
}
Bước 2.4: Lớp SinhVien.java
public class SinhVien {
private String hoTen;
// Đa hình: Khai báo kiểu dữ liệu của lớp Cha DiemHP
private DiemHP hp1;
private DiemHP hp2;
public SinhVien(String hoTen, DiemHP hp1, DiemHP hp2) {
this.hoTen = hoTen;
this.hp1 = hp1;
this.hp2 = hp2;
}
// Tính điểm trung bình theo số tín chỉ
public double tinhDTB() {
double diem1 = hp1.tinhDiem(); // Liên kết muộn: Tự gọi tinhDiem() tương ứng của Lý thuyết/Thực hành
double diem2 = hp2.tinhDiem();
int tc1 = hp1.laySoTinChi();
int tc2 = hp2.laySoTinChi();
return (diem1 * tc1 + diem2 * tc2) / (tc1 + tc2);
}
public void inThongTin() {
System.out.println("Họ tên SV: " + hoTen);
System.out.printf(" + HP1: %-15s | Điểm: %-4.1f | Số TC: %d\n", hp1.layTenHP(), hp1.tinhDiem(), hp1.laySoTinChi());
System.out.printf(" + HP2: %-15s | Điểm: %-4.1f | Số TC: %d\n", hp2.layTenHP(), hp2.tinhDiem(), hp2.laySoTinChi());
System.out.printf(" => Điểm trung bình: %.2f\n\n", tinhDTB());
}
}
Bước 2.5: Viết MainLab05.java (Thực thi bài tập)
public class MainLab05 {
public static void main(String[] args) {
// Tạo mảng 3 sinh viên với đa dạng các học phần Lý thuyết và Thực hành
SinhVien[] dsSV = new SinhVien[3];
// SV 1: Học 2 môn Lý thuyết
DiemHP hp1_1 = new HPLyThuyet("Toán cao cấp", 3, 9, 8, 7);
DiemHP hp1_2 = new HPLyThuyet("Kỹ thuật lập trình", 4, 10, 9, 9);
dsSV[0] = new SinhVien("Nguyen Van An", hp1_1, hp1_2);
// SV 2: Học 1 môn Lý thuyết, 1 môn Thực hành
DiemHP hp2_1 = new HPLyThuyet("Triết học", 2, 8, 7, 6);
DiemHP hp2_2 = new HPThucHanh("Lập trình Java", 3, 8.5, 9.0, 9.5);
dsSV[1] = new SinhVien("Tran Thi Binh", hp2_1, hp2_2);
// SV 3: Học 2 môn Thực hành
DiemHP hp3_1 = new HPThucHanh("Mạng máy tính TH", 2, 7.0, 8.0, 8.5);
DiemHP hp3_2 = new HPThucHanh("Cơ sở dữ liệu TH", 2, 9.0, 9.5, 10.0);
dsSV[2] = new SinhVien("Le Van Cuong", hp3_1, hp3_2);
// In thông tin lên màn hình
System.out.println("=== KẾT QUẢ HỌC TẬP CỦA SINH VIÊN ===");
for (SinhVien sv : dsSV) {
sv.inThongTin();
}
}
}
Bổ sung: hiểu sâu từng dòng (Bài 5A)
Từ mới trong bài này (giải từ số 0):
- abstract (trừu tượng): lớp dở dang, không được
newtrực tiếp.DiemHPnói "mọi học phần đều tính được điểm" nhưng chưa nói tính thế nào — để lớp con tự định nghĩa. - Phương thức trừu tượng: hàm chỉ có tên, không có ruột (
public abstract double tinhDiem();— chú ý dấu;thay vì{...}). - extends (kế thừa): lớp con nhận hết của nả lớp cha rồi thêm đồ riêng.
HPLyThuyet extends DiemHPnên có sẵntenHP, soTinChimà không cần khai báo lại. - super(...): gọi constructor của lớp cha. Dòng đầu tiên trong constructor con phải nhờ cha dựng phần của cha trước.
- protected: mức khóa ở giữa — lớp con được sờ trực tiếp (
tenHP), người ngoài thì không. - @Override: tem dán lên hàm viết lại của cha, để trình biên dịch bắt lỗi nếu gõ sai tên/tham số.
- Đa hình + liên kết muộn: biến kiểu cha (
DiemHP hp1) nhưng trỏ object con (new HPLyThuyet(...)). Khi gọihp1.tinhDiem(), Java tự chọn đúng ruột của con vào giờ chạy — không cần if-else hỏi "mày là lý thuyết hay thực hành?".
Mổ xẻ điểm trung bình theo tín chỉ:
double diem1 = hp1.tinhDiem(); // liên kết muộn:_hp1 là lý thuyết thì chạy công thức 0.1/0.2/0.7,
double diem2 = hp2.tinhDiem(); // là thực hành thì chạy trung bình 3 bài — cùng một dòng gọi!
int tc1 = hp1.laySoTinChi();
int tc2 = hp2.laySoTinChi();
return (diem1 * tc1 + diem2 * tc2) / (tc1 + tc2); // trung bình có trọng số
Trace tay SV An: HP1 Toán cao cấp (9, 8, 7) → 9×0.1 + 8×0.2 + 7×0.7 = 0.9 + 1.6 + 4.9 = 7.4 (3 tín chỉ). HP2 KTLT (10, 9, 9) → 1.0 + 1.8 + 6.3 = 9.1 (4 tín chỉ). DTB = (7.4×3 + 9.1×4)/7 = 58.6/7 = 8.37 — khớp output.
Tự kiểm tra:
new DiemHP("Toan", 3)thì sao?
Đáp án
Biên dịch lỗi ngay — lớp abstract cấmnew trực tiếp. Phải new qua lớp con như HPLyThuyet.- Quên
@OverridetrướctinhDiem()ở lớp con thì sao?
Đáp án
Code vẫn chạy nếu gõ đúng tên. Nhưng nếu gõ sai (ví dụtinhDiemm) mà không có tem thì Java tưởng đó là hàm mới, lớp con vẫn thiếu ruột → lỗi khó hiểu. Có tem thì lỗi lộ ngay khi biên dịch.- Thêm loại học phần mới (đồ án) có phải sửa
SinhVienkhông?
Đáp án
Không — chỉ cần lớp mới extends DiemHP (xem bài tập HPDuan ở cuối phần này).SinhVien ôm kiểu cha nên nhận luôn. Đó chính là sức mạnh của đa hình.