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
- Validation (kiểm tra hợp lệ): setter không nhận bừa, số sai thì giữ giá trị cũ.
- Trọng số: cột nào quan trọng thì nhân hệ số to. Cuối kỳ hệ số 0.7 — 1 điểm cuối kỳ đáng giá gấp 7 lần chuyên cần.
Ý tưởng
- Thuộc tính
privatehết. - Mỗi setter kiểm tra miền rồi mới gán.
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
- Gán trực tiếp trong constructor (
this.soTinChi = soTinChi): dựng môn 9 tín chỉ vẫn lọt, luật gác cổng thành đồ trang trí. - Setter báo lỗi bằng
println: setter tốt im lặng từ chối, code gọi tự kiểm tra bằng getter nếu cần.
Tự luyện
- Thêm
xepLoai()cho môn: ≥8 Giỏi, ≥6.5 Khá, ≥5 TB, còn lại Yếu. - Thêm
dat()trảtruenế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
- HAS-A (quan hệ "có một"):
SinhViencó cácDiem. Code thể hiện bằng thuộc tính kiểu lớp khác:private Diem dm1, dm2;. - Trung bình trọng số: môn nhiều tín chỉ "nặng" hơn. Cộng đều rồi chia 2 là sai.
Ý tưởng
- Lấy số tín chỉ từng môn.
- Tổng cả hai bằng 0 → trả 0 (chặn chia 0).
(đ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
(7.4 + 7.6) / 2 = 7.5: sai vì môn 3 TC phải nặng hơn môn 2 TC.- Chia nguyên: tử số
intlà mất lẻ. Ở đâytinhDiem()trảdoublenên cả biểu thức thànhdouble— đúng một cách tình cờ, nhưng nên hiểu vì sao đúng.
Tự luyện
- Thêm
hocBong(): ĐTB ≥ 8 và không môn nào dưới 6.5 thìtrue. - 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
- Mảng + số lượng thật:
new Diem[soMonToiDa]toàn ô rỗng, biếnsoHPđếm số môn đã thêm thật. Mọi vòng lặp chỉ chạy tớisoHP— mẫu chuẩn gặp lại ở Lab 04, 05. null: ô trống, "không trỏ vào object nào".
Ý tưởng
themHP: null hoặc đầy →false; còn chỗ → gán vàodsHP[soHP], tăng đếm.tinhDTB: cộng dồnđiểm*tín chỉ, chia tổng tín chỉ.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
- Duyệt tới
dsHP.lengththay vìsoHP: chạm ônulllàNullPointerExceptionngay. - Quên
soHP++: thêm 10 môn mà đếm vẫn 0, tính ĐTB chia 0.
Tự luyện
- Thêm
xoaHP(String tenMon): xóa môn theo tên (mượn cách dồn mảng Lab 04). - Thêm
monCaoNhat(): trả vềDiemcó đ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ái sử dụng:
HCNkhông lưu 4 số rời rạc mà lưu 2 objectDiemToaDo. Có lớp Điểm rồi thì dùng, đừng làm lại. Math.sqrt: căn bậc hai.Math.abs: trị tuyệt đối.
Ý tưởng
- Khoảng cách theo Pi-ta-go:
sqrt(dx² + dy²). - Dài =
|x2 - x1|, rộng =|y2 - y1|. - Chu vi =
2*(dài + rộng), diện tích =dài * rộng. - 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
- Quên
Math.abs: góc truyền ngược cho dài âm, diện tích âm, sắp xếp loạn. - So diện tích
doublebằng==khi sắp xếp: dùng>như code mẫu, không dùng==.
Tự luyện
- Thêm
hinhVuong():truenếu dài bằng rộng (nhớ epsilon!). - 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.