Khóa OOP người mới: Bài 5A — Điểm học phần

Bài 5 thuộc Lab 05: lớp trừu tượng, kế thừa và đa hình qua điểm học phần và thùng thư.

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):

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:

  1. new DiemHP("Toan", 3) thì sao?
Đáp ánBiên dịch lỗi ngay — lớp abstract cấm new trực tiếp. Phải new qua lớp con như HPLyThuyet.
  1. Quên @Override trước tinhDiem() ở lớp con thì sao?
Đáp ánCode 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.
  1. Thêm loại học phần mới (đồ án) có phải sửa SinhVien không?
Đáp ánKhô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.