Lab 03 cho người mới: điểm, sinh viên, hình chữ nhật

Lab này dạy thiết kế lớp: gói dữ liệu + việc thành object, lớp chứa lớp (HAS-A), setter gác cổng. Ba cụm: điểm–sinh viên, bản mở rộng nhiều học phần, tọa độ–hình chữ nhật.

Bài 1. Diem — setter gác cổng

Đề nói gì

Lớp Diem lưu tên môn, số tín chỉ và 3 cột điểm (chuyên cần, giữa kỳ, cuối kỳ). Điểm ngoài 0–10 và tín chỉ ngoài 1–4 phải bị từ chối. Điểm môn = cc*0.1 + gk*0.2 + ck*0.7.

Từ mới

Ý tưởng

  1. Thuộc tính private hết.
  2. Mỗi setter kiểm tra miền rồi mới gán.
  3. tinhDiem() áp công thức trọng số.

Code mổ xẻ

public Diem(String tenHocPhan, int soTinChi) {
    this.tenHocPhan = tenHocPhan;
    setSoTinChi(soTinChi);   // DÙNG LẠI setter để được kiểm tra miễn phí
}
public void setSoTinChi(int tc) {
    if (tc >= 1 && tc <= 4) this.soTinChi = tc; // sai thì lờ đi
}
public void setCuoiKy(double d) { if (d >= 0 && d <= 10) cuoiKy = d; }

Constructor gọi setSoTinChi thay vì gán trực tiếp — luật kiểm tra chỉ viết một chỗ mà cả dựng mới lẫn sửa sau đều được hưởng. Thử trong code mẫu: d1.setCuoiKy(15) bị từ chối, getCuoiKy() vẫn là 7. Khớp output mẫu.

Chạy tay

Môn Java (3 TC): chuyên cần 9, giữa kỳ 8, cuối kỳ 7 → 9*0.1 + 8*0.2 + 7*0.7 = 0.9 + 1.6 + 4.9 = 7.4. toString() làm tròn 2 chữ số rồi in Lap trinh Java (3 TC): 7.4.

Bẫy hay gặp

Tự luyện

  1. Thêm xepLoai() cho môn: ≥8 Giỏi, ≥6.5 Khá, ≥5 TB, còn lại Yếu.
  2. Thêm dat() trả true nếu điểm môn ≥ 4 (qua môn).

Bài 2. SinhVien — trung bình trọng số tín chỉ

Đề nói gì

Mỗi sinh viên có 2 môn (Diem). Điểm tích lũy (ĐTB) là trung bình có trọng số tín chỉ. In 3 sinh viên mẫu ra đúng 7.48, 7.12, 7.52.

Từ mới

Ý tưởng

  1. Lấy số tín chỉ từng môn.
  2. Tổng cả hai bằng 0 → trả 0 (chặn chia 0).
  3. (điểm1*tc1 + điểm2*tc2) / (tc1 + tc2).

Code mổ xẻ

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);
}

tinhDTB không tự tính điểm môn — nó nhờ dm1.tinhDiem(). Mỗi lớp lo việc của mình, lớp ngoài chỉ phối hợp. Đây là tư duy OOP.

Chạy tay (bạn An)

Môn Java (3 TC): 7.4 (tính ở bài 1). Môn Toán (2 TC): 6*0.1 + 7*0.2 + 8*0.7 = 0.6 + 1.4 + 5.6 = 7.6. ĐTB = (7.4*3 + 7.6*2) / 5 = (22.2 + 15.2) / 5 = 37.4 / 5 = 7.48. Khớp output mẫu. Hai bạn còn lại tính y hệt ra 7.12 và 7.52 — tự chạy tay một bạn để chắc.

Bẫy hay gặp

Tự luyện

  1. Thêm hocBong(): ĐTB ≥ 8 và không môn nào dưới 6.5 thì true.
  2. Sắp xếp 3 sinh viên theo ĐTB giảm dần (mượn nổi bọt Lab 01).

Bài 3. SinhVienMoRong — danh sách trên mảng

Đề nói gì

Sinh viên có nhiều môn (không cố định 2). Thêm môn (từ chối null và khi đầy), tính ĐTB, in ra 7.82.

Từ mới

Ý tưởng

  1. themHP: null hoặc đầy → false; còn chỗ → gán vào dsHP[soHP], tăng đếm.
  2. tinhDTB: cộng dồn điểm*tín chỉ, chia tổng tín chỉ.
  3. toString: 1 dòng tổng + mỗi môn 1 dòng.

Code mổ xẻ

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

Thứ tự d == null trước là cố ý: Java duyệt || từ trái, null thì dừng ngay, không bao giờ chạm dsHP sau đó. Test mẫu: Them null: false (bị từ chối), Them 3: true (nhận).

Chạy tay và bí ẩn 7.82

Máy tính lưu số thực dạng nhị phân nên 7.82 thật ra là 7.8249999..., làm tròn 2 chữ số còn 7.82 chứ không phải 7.83. Bài học: không bao giờ so double bằng ==; muốn bằng nhau thì so hiệu nhỏ hơn epsilon (Math.abs(a-b) < 1e-9).

Bẫy hay gặp

Tự luyện

  1. Thêm xoaHP(String tenMon): xóa môn theo tên (mượn cách dồn mảng Lab 04).
  2. Thêm monCaoNhat(): trả về Diem có điểm cao nhất.

Bài 4. DiemToaDo và HCN — tái sử dụng lớp

Đề nói gì

Điểm tọa độ (x, y) có khoảng cách tới điểm khác. Hình chữ nhật xác định bởi 2 điểm góc đối, tính chu vi/diện tích, sắp xếp 5 hình theo diện tích tăng dần.

Từ mới

Ý tưởng

  1. Khoảng cách theo Pi-ta-go: sqrt(dx² + dy²).
  2. Dài = |x2 - x1|, rộng = |y2 - y1|.
  3. Chu vi = 2*(dài + rộng), diện tích = dài * rộng.
  4. Sắp xếp nổi bọt theo diện tích.

Code mổ xẻ

public double khoangCach(DiemToaDo p) {
    double dx = this.x - p.x;   // this: điểm hiện tại; p: điểm truyền vào
    double dy = this.y - p.y;
    return Math.sqrt(dx * dx + dy * dy);
}
public double chieuDai()  { return Math.abs(d2.getX() - d1.getX()); }

Math.abs để truyền góc theo thứ tự nào cũng ra dương. Muốn dài/rộng thì hỏi điểm qua getter (d1.getX()), không thò tay vào thuộc tính private của lớp khác.

Chạy tay

Hình (0,4)-(3,0): dài |3-0| = 3, rộng |0-4| = 4, chu vi 2*(3+4) = 14, diện tích 12. Hình (0,3)-(2,0): dài 2 rộng 3, chu vi 2*(2+3) = 10, diện tích 6. Cặp 18/20 trong output mẫu là hình (1,5)-(6,1): dài 5 rộng 4, chu vi 2*9 = 18, diện tích 20. Tự tính 2 hình còn lại rồi đối chiếu thứ tự sau khi sắp xếp theo diện tích.

Bẫy hay gặp

Tự luyện

  1. Thêm hinhVuong(): true nếu dài bằng rộng (nhớ epsilon!).
  2. Thêm chua(DiemToaDo p): điểm có nằm trong hình không.

Phụ lục OOP: HAS-A và dây chuyền tính điểm

Sơ đồ object bài SinhVien

new SinhVien("An", d1, d2) — object An nắm 2 tham chiếu tới 2 object Diem đã đúc sẵn. Vẽ ra giấy:

SinhVien "An" ──dm1──▶ Diem(Java, 3TC, 7.4)
              └─dm2──▶ Diem(Toán, 2TC, 7.6)

Đây là HAS-A ("có một"): An có các điểm môn. Khác IS-A (kế thừa) ở chỗ An không là Diem, An chỉ giữ Diem. HCN cũng HAS-A 2 DiemToaDo. Gặp lại ở Lab 04 (DSSV giữ SV[]) và Lab 05 (SinhVien giữ DiemHP).

Một lời gọi, cả dây chuyền chạy

sinhVien.tinhDTB() không tự tính mà hỏi từng môn:

tinhDTB() → dm1.tinhDiem() (= 7.4) → dm2.tinhDiem() (= 7.6) → ráp công thức → 7.48

Mỗi object chỉ biết việc của mình: Diem biết điểm môn, SinhVien biết ráp tín chỉ. Muốn đổi cách tính môn (thêm cột điểm) chỉ sửa Diem — SinhVien không đụng một chữ. Đây là phân rã trách nhiệm, lợi ích lớn nhất của OOP so với viết một hàm dài.

Tham chiếu chung, sửa một nơi thấy mọi nơi

d1 vừa nằm trong ds[0] vừa là biến riêng ở main. d1.setCuoiKy(15) bị từ chối — nhưng nếu là điểm hợp lệ, ds[0].tinhDTB() tính lại thấy ngay vì cả hai cùng trỏ một object. Hai biến, một object: sửa qua biến nào cũng như nhau. Ngược lại new PS(...) trong các phép toán đúc object mới tinh — sửa object mới không ảnh hưởng object cũ. Phân biệt được hai kiểu này là hết 80% bug tham chiếu của người mới.