Lab 03 — Điểm học phần, Sinh viên, HCN

LAB 03 — GIẢI CHI TIẾT (ĐIỂM HỌC PHẦN, SINH VIÊN, HÌNH CHỮ NHẬT)

Nguồn: lab03.doc (Bài thực hành số 3). Bám lý thuyết buổi 8, 10, 11. Gồm 3 phần: bài chính (Diem + SinhVien), phần mở rộng (nhiều học phần), bài ở nhà (Diem toạ độ + HCN).

0. Bản đồ bài làm

Phần Lớp Tệp Quan hệ
Chính Diem, SinhVien Diem.java, SinhVien.java, QuanLySV.java HAS-A (SinhVien có 2 Điểm)
Mở rộng SinhVienMoRong SinhVienMoRong.java + QuanLySV2.java HAS-A với mảng
Ở nhà DiemToaDo, HCN DiemToaDo.java, HCN.java, QuanLyHCN.java HAS-A (HCN có 2 điểm)

Cảnh báo trùng tên: bài chính có lớp Diem (điểm học phần), bài ở nhà cũng có lớp Diem (toạ độ). Trong tài liệu này bài ở nhà dùng tên DiemToaDo để hai lớp không đè nhau trong cùng thư mục. Khi nộp, bạn có thể đổi lại thành Diem và để trong thư mục riêng.


PHẦN 1 — Diem VÀ SinhVien

0. Đề bài

Khai báo lớp Diem: tenHocPhan, soTinChi (1..4), chuyenCan, giuaKy, cuoiKy (0..10); constructor; tinhDiem() theo tỷ lệ 10% – 20% – 70%. Khai báo lớp SinhVien: hoTen, dm1, dm2 (hai Diem); constructor; tinhDTB() trung bình theo tín chỉ. Tạo mảng 3 sinh viên, in thông tin và điểm trung bình.

1. Yêu cầu ẩn

2. Input → Output

Lớp Thuộc tính Kiểu Miền Phương thức chính
Diem tenHocPhan String khác rỗng tinhDiem()
soTinChi int 1..4
chuyenCan/giuaKy/cuoiKy double 0..10
SinhVien hoTen String tinhDTB()
dm1, dm2 Diem object hợp lệ

3. Thiết kế

          Diem                          SinhVien
  - tenHocPhan : String            - hoTen : String
  - soTinChi : int                 - dm1 : Diem
  - chuyenCan, giuaKy, cuoiKy      - dm2 : Diem
  + Diem(ten, tc)                  + SinhVien(hoTen, dm1, dm2)
  + set/get (co kiem tra)          + tinhDTB() : double
  + tinhDiem() : double            + toString()
  + toString() : String
         ^                                   |
         |                                   | chứa (HAS-A)
         +-----------------------------------+

Quyết định thiết kế:

  1. HAS-A, không phải IS-A: SinhVien có điểm, không là điểm → dùng thuộc tính, không extends.
  2. Constructor Diem(ten, tc) gọi setter để dùng chung logic kiểm tra (tránh viết 2 lần).
  3. tinhDTB() đặt trong SinhVien (không phải Diem) vì nó phụ thuộc nhiều học phần.

4. Code từng bước

Bước 1 — khung Diem + constructor có kiểm tra.

public class Diem {
    private String tenHocPhan;
    private int soTinChi;
    private double chuyenCan, giuaKy, cuoiKy;

    public Diem(String tenHocPhan, int soTinChi) {
        this.tenHocPhan = tenHocPhan;
        setSoTinChi(soTinChi);
    }

    public void setSoTinChi(int tc) {
        if (tc >= 1 && tc <= 4) this.soTinChi = tc;
    }

Bước 2 — setter cho 3 loại điểm (cùng một điều kiện).

    public void setChuyenCan(double d) { if (d >= 0 && d <= 10) chuyenCan = d; }
    public void setGiuaKy(double d)    { if (d >= 0 && d <= 10) giuaKy = d; }
    public void setCuoiKy(double d)    { if (d >= 0 && d <= 10) cuoiKy = d; }

Bước 3 — công thức tính điểm + toString.

    public double tinhDiem() {
        return chuyenCan * 0.1 + giuaKy * 0.2 + cuoiKy * 0.7;
    }

    @Override
    public String toString() {
        double d = Math.round(tinhDiem() * 100) / 100.0;
        return tenHocPhan + " (" + soTinChi + " TC): " + d;
    }

Bước 4 — SinhVien và công thức trung bình có trọng số.

public class SinhVien {
    private String hoTen;
    private Diem dm1, dm2;

    public SinhVien(String hoTen, Diem dm1, Diem dm2) {
        this.hoTen = hoTen;
        this.dm1 = dm1;
        this.dm2 = dm2;
    }

    public double tinhDTB() {
        int tc1 = dm1.getSoTinChi(), tc2 = dm2.getSoTinChi();
        if (tc1 + tc2 == 0) return 0;                 // chan chia 0
        return (dm1.tinhDiem() * tc1 + dm2.tinhDiem() * tc2) / (tc1 + tc2);
    }
}

5. Lời giải hoàn chỉnh

// ===== Diem.java =====
public class Diem {
    private String tenHocPhan;
    private int soTinChi;
    private double chuyenCan, giuaKy, cuoiKy;

    public Diem(String tenHocPhan, int soTinChi) {
        this.tenHocPhan = tenHocPhan;
        setSoTinChi(soTinChi);
    }

    public String getTenHocPhan() { return tenHocPhan; }
    public int getSoTinChi()      { return soTinChi; }

    public void setSoTinChi(int tc) {
        if (tc >= 1 && tc <= 4) this.soTinChi = tc;
    }

    public double getChuyenCan() { return chuyenCan; }
    public void setChuyenCan(double d) { if (d >= 0 && d <= 10) chuyenCan = d; }

    public double getGiuaKy() { return giuaKy; }
    public void setGiuaKy(double d) { if (d >= 0 && d <= 10) giuaKy = d; }

    public double getCuoiKy() { return cuoiKy; }
    public void setCuoiKy(double d) { if (d >= 0 && d <= 10) cuoiKy = d; }

    public double tinhDiem() {
        return chuyenCan * 0.1 + giuaKy * 0.2 + cuoiKy * 0.7;
    }

    @Override
    public String toString() {
        double d = Math.round(tinhDiem() * 100) / 100.0;
        return tenHocPhan + " (" + soTinChi + " TC): " + d;
    }
}
// ===== SinhVien.java =====
public class SinhVien {
    private String hoTen;
    private Diem dm1, dm2;

    public SinhVien(String hoTen, Diem dm1, Diem dm2) {
        this.hoTen = hoTen;
        this.dm1 = dm1;
        this.dm2 = dm2;
    }

    public String getHoTen() { return hoTen; }
    public Diem getDm1()     { return dm1; }
    public Diem getDm2()     { return dm2; }

    public double tinhDTB() {
        int tc1 = dm1.getSoTinChi();
        int tc2 = dm2.getSoTinChi();
        if (tc1 + tc2 == 0) return 0;
        return (dm1.tinhDiem() * tc1 + dm2.tinhDiem() * tc2) / (tc1 + tc2);
    }

    @Override
    public String toString() {
        double dtb = Math.round(tinhDTB() * 100) / 100.0;
        return hoTen + " | " + dm1 + " | " + dm2 + " | DTB = " + dtb;
    }
}
// ===== QuanLySV.java =====
public class QuanLySV {
    static Diem taoDiem(String ten, int tc, double cc, double gk, double ck) {
        Diem d = new Diem(ten, tc);
        d.setChuyenCan(cc);
        d.setGiuaKy(gk);
        d.setCuoiKy(ck);
        return d;
    }

    public static void main(String[] args) {
        Diem d1 = taoDiem("Lap trinh Java", 3, 9, 8, 7);
        Diem d2 = taoDiem("Toan roi rac",   2, 6, 7, 8);
        Diem d3 = taoDiem("Co so du lieu",  3, 10, 9, 8);
        Diem d4 = taoDiem("Ky nang mem",    2, 5, 6, 5);
        Diem d5 = taoDiem("Cau truc du lieu", 4, 7, 7, 7);
        Diem d6 = taoDiem("Thuc tap",       1, 8, 9, 10);

        SinhVien[] ds = new SinhVien[3];
        ds[0] = new SinhVien("Nguyen Van An",   d1, d2);
        ds[1] = new SinhVien("Tran Thi Binh",   d3, d4);
        ds[2] = new SinhVien("Le Van Chi",      d5, d6);

        for (int i = 0; i < ds.length; i++) {
            System.out.println(ds[i]);
        }

        // kiem tra validation: gan diem sai
        d1.setCuoiKy(15);
        System.out.println("Cuoi ky sau khi gan 15: " + d1.getCuoiKy());
    }
}

6. Cách chạy

javac Diem.java SinhVien.java QuanLySV.java
java QuanLySV

7. Kết quả chạy mẫu

Nguyen Van An | Lap trinh Java (3 TC): 7.4 | Toan roi rac (2 TC): 7.6 | DTB = 7.48
Tran Thi Binh | Co so du lieu (3 TC): 8.4 | Ky nang mem (2 TC): 5.2 | DTB = 7.12
Le Van Chi | Cau truc du lieu (4 TC): 7.0 | Thuc tap (1 TC): 9.6 | DTB = 7.52
Cuoi ky sau khi gan 15: 7.0

Kiểm tra tay:

8. Bảng kiểm thử

# Ca Kỳ vọng
1 tc = 3, 9/8/7 7.4
2 tc = 4 với 7/7/7 7.0
3 setSoTinChi(5) giữ nguyên giá trị cũ
4 setCuoiKy(15) giữ nguyên
5 Tổng tín chỉ 0 (dữ liệu sai) tinhDTB() trả 0, không chia 0

9. Trace

SV "Le Van Chi":

Bước tc1 tc2 diem1 diem2 Tử số Mẫu DTB
lấy giá trị 4 1 7.0 9.6 — — —
nhân trọng số 28.0 9.6 37.6 5 7.52

10. Lỗi thường gặp

Code sai Thông báo / hiện tượng Sửa
tinhDTB = (d1 + d2)/2 Sai khi số tín chỉ khác nhau Nhân trọng số
class SinhVien extends Diem Vô nghĩa về mô hình Dùng thuộc tính (HAS-A)
Gán d1.cuoiKy = 15 cuoiKy has private access Dùng setter
setSoTinChi không kiểm tra soTinChi = 0 → chia 0 Kiểm tra 1..4
In d1 không có toString Diem@1b6d3586 Override toString
return (d1*3 + d2*2) / 5; với double Không sai nhưng cứng nhắc Dùng tc1, tc2

11. Câu hỏi kiểm tra hiểu

  1. Vì sao SinhVien không nên extends Diem?
  2. Nếu hai học phần cùng 3 tín chỉ, công thức rút gọn thành gì?
  3. Vì sao constructor Diem nên gọi setSoTinChi thay vì gán trực tiếp?

12. Mở rộng

TỰ KIỂM TRA 60 GIÂY: SinhVien và Diem là IS-A hay HAS-A? 9/8/7 với 3 tín chỉ ra bao nhiêu? Ai kiểm tra điểm 0..10?


PHẦN 2 — MỞ RỘNG: NHIỀU HỌC PHẦN

0. Đề bài (phần "Mở rộng")

Một sinh viên có thể học nhiều học phần; mở rộng SinhVien để lưu được điểm nhiều học phần và tính DTB.

1. Yêu cầu ẩn

2. Thiết kế

        SinhVienMoRong
  - hoTen : String
  - dsHP : Diem[]
  - soHP : int
  + themHP(Diem) : boolean
  + tinhDTB() : double

3–4. Các bước chính

Bước 1 — thêm học phần có kiểm tra sức chứa:

public boolean themHP(Diem d) {
    if (d == null || soHP >= dsHP.length) return false;
    dsHP[soHP] = d;
    soHP++;
    return true;
}

Bước 2 — DTB có trọng số:

public double tinhDTB() {
    double tongDiem = 0;
    int tongTC = 0;
    for (int i = 0; i < soHP; i++) {
        tongDiem += dsHP[i].tinhDiem() * dsHP[i].getSoTinChi();
        tongTC   += dsHP[i].getSoTinChi();
    }
    return (tongTC == 0) ? 0 : tongDiem / tongTC;
}

5. Lời giải hoàn chỉnh

// ===== SinhVienMoRong.java =====
public class SinhVienMoRong {
    private String hoTen;
    private Diem[] dsHP;
    private int soHP;

    public SinhVienMoRong(String hoTen, int soMonToiDa) {
        this.hoTen = hoTen;
        this.dsHP = new Diem[soMonToiDa];
        this.soHP = 0;
    }

    public String getHoTen() { return hoTen; }
    public int getSoHP()     { return soHP; }

    public boolean themHP(Diem d) {
        if (d == null || soHP >= dsHP.length) return false;
        dsHP[soHP] = d;
        soHP++;
        return true;
    }

    public double tinhDTB() {
        double tongDiem = 0;
        int tongTC = 0;
        for (int i = 0; i < soHP; i++) {
            tongDiem += dsHP[i].tinhDiem() * dsHP[i].getSoTinChi();
            tongTC   += dsHP[i].getSoTinChi();
        }
        return (tongTC == 0) ? 0 : tongDiem / tongTC;
    }

    @Override
    public String toString() {
        double dtb = Math.round(tinhDTB() * 100) / 100.0;
        String s = hoTen + " (" + soHP + " hoc phan, DTB = " + dtb + ")\n";
        for (int i = 0; i < soHP; i++) {
            s += "  " + dsHP[i] + "\n";
        }
        return s;
    }
}
// ===== QuanLySV2.java =====
public class QuanLySV2 {
    public static void main(String[] args) {
        SinhVienMoRong sv = new SinhVienMoRong("Nguyen Van An", 10);

        Diem d1 = new Diem("Lap trinh Java", 3);
        d1.setChuyenCan(9); d1.setGiuaKy(8); d1.setCuoiKy(7);

        Diem d2 = new Diem("Toan roi rac", 2);
        d2.setChuyenCan(6); d2.setGiuaKy(7); d2.setCuoiKy(8);

        Diem d3 = new Diem("Co so du lieu", 3);
        d3.setChuyenCan(10); d3.setGiuaKy(9); d3.setCuoiKy(8);

        System.out.println("Them 1: " + sv.themHP(d1));
        System.out.println("Them 2: " + sv.themHP(d2));
        System.out.println("Them 3: " + sv.themHP(d3));
        System.out.println("Them null: " + sv.themHP(null));

        System.out.println(sv);
    }
}

6. Cách chạy

javac Diem.java SinhVienMoRong.java QuanLySV2.java
java QuanLySV2

7. Kết quả chạy mẫu

Them 1: true
Them 2: true
Them 3: true
Them null: false
Nguyen Van An (3 hoc phan, DTB = 7.82)

  Lap trinh Java (3 TC): 7.4
  Toan roi rac (2 TC): 7.6
  Co so du lieu (3 TC): 8.4

Kiểm tra: (7.4×3 + 7.6×2 + 8.4×3) / 8 = (22.2 + 15.2 + 25.2) / 8 = 62.6 / 8 = 7.825. Nhưng máy in 7.82, không phải 7.83 — đây là bài học về số thực:

Bước Giá trị thực trong máy
tinhDiem() của 9/8/7 7.3999999999999995 (không phải 7.4)
Nhân 3 22.199999999999999
Tổng 3 môn 62.599999999999994
Chia 8 7.824999999999999
Math.round(x*100) 782 → in 7.82

Vì 0.1, 0.2, 0.7 không biểu diễn chính xác trong hệ nhị phân, kết quả hụt một chút so với 782.5, nên làm tròn xuống. Muốn có 7.83 đúng như toán học, phải làm tròn từng điểm trước khi tính DTB (hoặc dùng BigDecimal — ngoài phạm vi môn học). Đây là lý do đề thi thường tránh các giá trị rơi đúng .5.

8. Bảng kiểm thử

# Ca Kỳ vọng
1 Thêm 3 học phần true cả 3, DTB 7.82 (xem giải thích số thực ở mục 7)
2 Thêm null false
3 Thêm quá sức chứa false
4 Chưa thêm gì DTB = 0, không lỗi

9. Trace tinhDTB

i diem tc tongDiem tongTC
0 7.4 3 22.2 3
1 7.6 2 37.4 5
2 8.4 3 62.6 8
kết thúc 62.6 8 → 7.825

10. Lỗi thường gặp

Code sai Hiện tượng Sửa
dsHP = new Diem[10] rồi dsHP[i].set... NullPointerException new Diem(...) trước
return tongDiem / soHP Sai trọng số Chia tongTC
Không kiểm tra soHP >= length ArrayIndexOutOfBoundsException Kiểm tra
themHP không tăng soHP Luôn ghi đè ô 0 soHP++

11. Câu hỏi kiểm tra hiểu

  1. Vì sao phải có cả dsHP và soHP, không dùng dsHP.length?
  2. Nếu đề cho "tối đa 5 học phần", nên kiểm tra ở đâu?
  3. Công thức DTB có cần sắp xếp mảng không? Vì sao không?

12. Mở rộng


PHẦN 3 (Ở NHÀ) — DiemToaDo VÀ HCN

0. Đề bài

Lớp Diem: x, y (số thực); constructor; khoangCach(Diem p); toString() dạng (x, y). Lớp HCN: d1, d2 là góc trên-trái và dưới-phải; constructor; chuVi(), dienTich(); toString() dạng [(x1, y1), (x2, y2)]. Chương trình: mảng 5 hình chữ nhật, in kèm chu vi/diện tích, sắp xếp theo diện tích tăng dần.

1. Yêu cầu ẩn

2. Input → Output

Vào Kiểu Ra
x, y double điểm
d1, d2 Diem chuVi, dienTich

3. Thiết kế

      DiemToaDo                    HCN
  - x, y : double            - d1, d2 : DiemToaDo
  + DiemToaDo(x, y)          + HCN(d1, d2)
  + khoangCach(Diem)         + chieuDai() : double
  + toString()               + chieuRong() : double
                             + chuVi(), dienTich()
                             + toString()

HCN có hai điểm (HAS-A). Thêm chieuDai/chieuRong là phương thức phụ trợ private/public để tránh lặp công thức ở chuVi và dienTich.

4. Code từng bước

Bước 1 — DiemToaDo + khoảng cách.

public double khoangCach(DiemToaDo p) {
    double dx = this.x - p.x;
    double dy = this.y - p.y;
    return Math.sqrt(dx * dx + dy * dy);
}

@Override
public String toString() { return "(" + x + ", " + y + ")"; }

Bước 2 — cạnh của HCN (dùng trị tuyệt đối).

public double chieuDai()  { return Math.abs(d2.getX() - d1.getX()); }
public double chieuRong() { return Math.abs(d2.getY() - d1.getY()); }

Bước 3 — chu vi, diện tích.

public double chuVi()    { return 2 * (chieuDai() + chieuRong()); }
public double dienTich() { return chieuDai() * chieuRong(); }

Bước 4 — sắp xếp theo diện tích (bubble sort với dienTich()).

5. Lời giải hoàn chỉnh

// ===== DiemToaDo.java =====
public class DiemToaDo {
    private double x, y;

    public DiemToaDo(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() { return x; }
    public double getY() { return y; }

    public double khoangCach(DiemToaDo p) {
        double dx = this.x - p.x;
        double dy = this.y - p.y;
        return Math.sqrt(dx * dx + dy * dy);
    }

    @Override
    public String toString() { return "(" + x + ", " + y + ")"; }
}
// ===== HCN.java =====
public class HCN {
    private DiemToaDo d1;   // goc tren trai
    private DiemToaDo d2;   // goc duoi phai

    public HCN(DiemToaDo d1, DiemToaDo d2) {
        this.d1 = d1;
        this.d2 = d2;
    }

    public DiemToaDo getD1() { return d1; }
    public DiemToaDo getD2() { return d2; }

    public double chieuDai()  { return Math.abs(d2.getX() - d1.getX()); }
    public double chieuRong() { return Math.abs(d2.getY() - d1.getY()); }

    public double chuVi()    { return 2 * (chieuDai() + chieuRong()); }
    public double dienTich() { return chieuDai() * chieuRong(); }

    @Override
    public String toString() {
        return "[" + d1 + ", " + d2 + "]";
    }
}
// ===== QuanLyHCN.java =====
public class QuanLyHCN {
    public static void main(String[] args) {
        HCN[] ds = new HCN[5];
        ds[0] = new HCN(new DiemToaDo(0, 4), new DiemToaDo(3, 0));
        ds[1] = new HCN(new DiemToaDo(0, 3), new DiemToaDo(2, 0));
        ds[2] = new HCN(new DiemToaDo(1, 5), new DiemToaDo(6, 1));
        ds[3] = new HCN(new DiemToaDo(0, 2), new DiemToaDo(2, 0));
        ds[4] = new HCN(new DiemToaDo(0, 6), new DiemToaDo(1, 0));

        System.out.println("Danh sach ban dau:");
        for (int i = 0; i < ds.length; i++) {
            System.out.println(ds[i] + " chu vi = " + ds[i].chuVi()
                             + ", dien tich = " + ds[i].dienTich());
        }

        // sap xep theo dien tich tang dan
        for (int i = 0; i < ds.length - 1; i++) {
            for (int j = 0; j < ds.length - 1 - i; j++) {
                if (ds[j].dienTich() > ds[j + 1].dienTich()) {
                    HCN tmp = ds[j];
                    ds[j] = ds[j + 1];
                    ds[j + 1] = tmp;
                }
            }
        }

        System.out.println("Sau khi sap xep theo dien tich:");
        for (int i = 0; i < ds.length; i++) {
            System.out.println(ds[i] + " dien tich = " + ds[i].dienTich());
        }
    }
}

6. Cách chạy

javac DiemToaDo.java HCN.java QuanLyHCN.java
java QuanLyHCN

7. Kết quả chạy mẫu

Danh sach ban dau:
[(0.0, 4.0), (3.0, 0.0)] chu vi = 14.0, dien tich = 12.0
[(0.0, 3.0), (2.0, 0.0)] chu vi = 10.0, dien tich = 6.0
[(1.0, 5.0), (6.0, 1.0)] chu vi = 18.0, dien tich = 20.0
[(0.0, 2.0), (2.0, 0.0)] chu vi = 8.0, dien tich = 4.0
[(0.0, 6.0), (1.0, 0.0)] chu vi = 14.0, dien tich = 6.0
Sau khi sap xep theo dien tich:
[(0.0, 2.0), (2.0, 0.0)] dien tich = 4.0
[(0.0, 3.0), (2.0, 0.0)] dien tich = 6.0
[(0.0, 6.0), (1.0, 0.0)] dien tich = 6.0
[(0.0, 4.0), (3.0, 0.0)] dien tich = 12.0
[(1.0, 5.0), (6.0, 1.0)] dien tich = 20.0

Kiểm tra tay: H1 3×4 = 12, chu vi 2(3+4) = 14 ✓. H3 5×4 = 20, chu vi 2(5+4) = 18 ✓.

8. Bảng kiểm thử

# Ca Kỳ vọng
1 (0,4)-(3,0) diện tích 12, chu vi 14
2 (3,0)-(0,4) (đảo ngược) vẫn 12 / 14
3 Hai điểm trùng nhau diện tích 0, chu vi 0
4 khoangCach((0,0),(3,4)) 5.0
5 Sắp xếp 5 hình tăng dần theo diện tích

9. Trace khoangCach

d1(0,0), p(3,4): dx = -3, dy = -4, dx² + dy² = 9 + 16 = 25, √25 = 5.0.

10. Lỗi thường gặp

Code sai Hiện tượng Sửa
(d2.x - d1.x) * (d2.y - d1.y) cho chu vi Sai công thức 2*(dai+rong)
Không dùng Math.abs Cạnh âm → diện tích âm Trị tuyệt đối
ds[j] > ds[j+1] bad operand types for binary operator '>' So dienTich()
toString trả x + "," + y Sai định dạng đề "(" + x + ", " + y + ")"
Dùng == để so sánh điểm So địa chỉ So x, y

11. Câu hỏi kiểm tra hiểu

  1. Vì sao dùng Math.abs mà vẫn đúng khi người dùng nhập hai góc ngược?
  2. Nếu đề yêu cầu sắp xếp theo chu vi thì đổi đúng một chỗ nào?
  3. Vì sao không nên để main tự tính dienTich thay vì gọi ds[i].dienTich()?

12. Mở rộng


Checklist nộp Lab 03

🎧
Lồng tiếng tóm tắt bài giảng (ZeroTTS AI)
Đang đọc: Mai Chi (Nữ) · Thời lượng ~1–2 phút
ZeroTTS AI
00:00
--:--
📝 Lời thuyết minh (Tự động tô sáng câu đang đọc • Bấm vào câu để tua tới):
Đang nạp dữ liệu lời đọc...