Lab 02 cho người mới: phân số, ATM, phương trình, nhân viên
Bài đầu tiên có lớp do mình tự viết. Đọc theo thứ tự: phân số (lớp + rút gọn) → ATM (boolean + luật) → phương trình (liệt kê trường hợp) → nhân viên (công thức).
Bài 1. PS — lớp phân số viết từ đầu
Đề nói gì
Viết lớp PS lưu tử/mẫu với 20 phương thức:
dựng, chuẩn hóa, rút gọn, cộng trừ nhân chia (với số và với phân số),
so sánh, tăng, gấp. Chương trình mẫu tính p + q,
t = 2*p - p/q, so sánh, rồi sắp xếp 5 phân số.
Từ mới
- UCLN (ước chung lớn nhất): số lớn nhất chia hết cả tử lẫn mẫu.
6/9rút cho UCLN 3 thành2/3. - Quy đồng: đưa hai phân số về cùng mẫu để cộng trừ.
- Chuẩn hóa: ép mẫu luôn dương (mẫu âm thì đổi dấu cả hai).
Ý tưởng
- Dựng object, chặn mẫu 0, chuẩn hóa dấu.
- Rút gọn bằng Euclid (lấy dư liên tục).
- Cộng/trừ/nhân/chia theo công thức phân số đã học ở phổ thông.
- So sánh bằng nhân chéo, sắp xếp bằng nổi bọt.
Code mổ xẻ: dựng và chuẩn hóa
public PS(int tu, int mau) {
this.tu = tu;
this.mau = (mau == 0) ? 1 : mau; // mẫu 0 thì tự sửa thành 1
chuanHoa(); // dựng xong chuẩn hóa ngay
}
private void chuanHoa() {
if (mau < 0) { tu = -tu; mau = -mau; } // mẫu âm: đổi dấu cả hai
}
this.tu là thuộc tính của object, tu trơ trọi là tham số truyền vào.
private trên chuanHoa vì đây là việc nội bộ, code ngoài không cần gọi.
Code mổ xẻ: Euclid tìm UCLN
int a = Math.abs(tu), b = Math.abs(mau); // trị tuyệt đối, UCLN luôn dương
while (b != 0) { int r = a % b; a = b; b = r; }
if (a != 0) { tu /= a; mau /= a; }
Chạy tay 6/9: (a,b) = (6,9) → dư 6 → (9,6) → dư 3 → (6,3) →
dư 0 → (3,0) dừng. a = 3 là UCLN, chia ra 2/3.
if (a != 0) vì tử 0 thì UCLN tính ra 0, chia là chết.
Code mổ xẻ: hai kiểu phương thức
public PS cong(PS p) { ...; return new PS(...); } // kiểu 1: TRẢ VỀ phân số mới
public void rutGon() { ...; } // kiểu 2: SỬA object hiện tại
cong không đụng tới tu/mau cũ — p.cong(q) xong p vẫn nguyên.
rutGon (void) sửa thẳng object gọi nó. Dùng sai kiểu là bug:
quên r = ... khi gọi cong thì kết quả bay mất;
gọi rutGon mà tưởng nó trả về cái mới thì nhận void.
Chạy tay ví dụ kiểm tra
p = 1/2, q = 5/7.
p + q: quy đồng mẫu 14, tử 1*7 + 5*2 = 17 → 17/14.
t = 2*p - p/q: p.nhan(2) = 2/2 (chưa rút gọn!);
p.chia(q) = 1/2 ÷ 5/7 = 1*7 / 2*5 = 7/10;
2/2 - 7/10: quy đồng 20, tử 20 - 14 = 6 → 6/20,
rutGon() chia 2 → 3/10.
Nhiều bạn tính 2p = 2 (quên mẫu) nên ra 13/10 — sai ở đúng chỗ này.
So sánh 1/2 với 5/7: nhân chéo 1*7 = 7 < 10 = 5*2 → p < q.
Sắp xếp [1/2, 13/2, 5/7, 6/9, 30/23]: rút gọn trước
(6/9 → 2/3) rồi nổi bọt bằng soSanh → 1/2 2/3 5/7 30/23 13/2.
Khớp output mẫu.
Bẫy hay gặp
return nullkhi chia 0: code gọi phải kiểm tra null trước khi dùng, không làNullPointerException.- So phân số bằng
double:1/3lưu nhị phân thiếu chút, hai phân số bằng nhau có thể so ra khác. Nhân chéo thì tuyệt đối đúng.
Tự luyện
- Thêm
luyThua(int k)trả về phân số mũ k. - Thêm
honSo():7/3in ra2(1/3).
Bài 2. ATM — tiền nong phải chặn trước
Đề nói gì
Lớp ATM có số tài khoản và số dư. Nạp, rút, chuyển khoản.
Luật: số dư mở mới dưới 100 thì nâng lên 100;
rút/chuyển quá số dư thì từ chối (trả false).
Từ mới
boolean: kiểu đúng/sai. Hàm trảbooleanlà hàm "báo kết quả".static final: hằng số chung (SO_DU_TOI_THIEU = 100không đổi).finaltrên thuộc tính: gán 1 lần trong constructor, sau đó khóa (số tài khoản không bao giờ đổi).this:tk == thisnghĩa là "chuyển cho chính tôi".
Ý tưởng
- Constructor ép số dư tối thiểu.
napTien: tiền dương mới cộng.rutTien: tiền sai hoặc quá số dư →false; đủ → trừ,true.chuyenTien: chặn null/tự chuyển, rồi dùng lại rút + nạp.
Code mổ xẻ
public ATM(String soTK, double soDu) {
this.soTK = soTK;
this.soDu = (soDu < SO_DU_TOI_THIEU) ? SO_DU_TOI_THIEU : soDu;
}
public boolean rutTien(double st) {
if (st <= 0 || st > soDu) return false;
soDu -= st;
return true;
}
public boolean chuyenTien(ATM tk, double st) {
if (tk == null || tk == this) return false; // rỗng hoặc tự chuyển: từ chối
if (!rutTien(st)) return false; // rút thất bại thì dừng
tk.napTien(st); // rút được mới nạp bên kia
return true;
}
chuyenTien không kiểm tra số dư trực tiếp — nó tái sử dụng
rutTien (vốn đã kiểm tra). Viết vậy thì luật chỉ nằm một chỗ,
sửa một chỗ là xong cả hai. Dấu ! là phủ định: !rutTien(st)
nghĩa là "rút thất bại".
Chạy tay
5 tài khoản mở 1000, 100, 100, 50, 100 → TK 50 thành 100.
Tổng đầu 1400. Nạp 1000 vào TK1 → 2000.
Chuyển 500 sang TK2: TK1 1500, TK2 600, in true.
TK2 rút 200 → 400, in true. Rút 99999 → quá số dư, in false.
Tổng cuối 1500 + 400 + 100 + 100 + 100 = 2200.0. Khớp output mẫu.
Bẫy hay gặp
- Nạp trước, kiểm tra sau:
tk.napTienrồi mới phát hiện rút lỗi là tiền từ trên trời rơi xuống. Thứ tự rút-trước-nạp-sau là bắt buộc. - Quên chặn
tk == this: tự chuyển cho mình vẫntrue, tuy tiền không đổi nhưng log giao dịch sai.
Tự luyện
- Thêm phí rút 1000đ mỗi lần (trừ vào số dư, vẫn phải đủ số dư).
- Thêm
saoKe(): in số dư sau mỗi lần nạp/rút của tài khoản đó.
Bài 3. PTB2 — liệt kê trường hợp rồi mới code
Đề nói gì
Giải ax² + bx + c = 0 với mọi a, b, c nhập vào,
kể cả suy biến (a = 0).
Từ mới
- Delta =
b² - 4ac: dấu của nó quyết định số nghiệm. - Suy biến:
a = 0thì hết là bậc hai, rớt xuống bậc một.
Ý tưởng (viết ra giấy trước khi code)
a == 0:b == 0nữa thìc == 0vô số nghiệm, ngược lại vô nghiệm;bkhác 0 thìx = -c/b.akhác 0: delta âm vô nghiệm, bằng 0 nghiệm kép, dương hai nghiệm.
Code mổ xẻ
public String giai() { // TRẢ VỀ chuỗi, không in
if (a == 0) {
if (b == 0) return (c == 0) ? "Vo so nghiem" : "Vo nghiem";
return "Bac 1, nghiem x = " + (-c / b);
}
double delta = b * b - 4 * a * c;
if (delta < 0) return "Vo nghiem";
if (delta == 0) return "Nghiem kep x = " + (-b / (2 * a));
double x1 = (-b + Math.sqrt(delta)) / (2 * a);
double x2 = (-b - Math.sqrt(delta)) / (2 * a);
return "x1 = " + x1 + ", x2 = " + x2;
}
return trong hàm có giá trị nghĩa là "đáp án là chuỗi này".
(-c / b) chia double nên 0 2 -4 ra đúng 2.0.
Math.sqrt là căn bậc hai.
Chạy tay
1 -3 2: delta 9 - 8 = 1, x1 = (3+1)/2 = 2.0, x2 = (3-1)/2 = 1.0.
1 2 1: delta 4 - 4 = 0, nghiệm kép -2/2 = -1.0.
1 0 1: delta -4, vô nghiệm. 0 0 0: vô số nghiệm. 0 0 5: vô nghiệm.
Cả 6 ca test khớp.
Bẫy hay gặp
- Nhảy vào tính delta ngay, quên
a == 0: nhập0 2 -4là chia 0. - So
delta == 0với số thực: ở đây delta từ số đẹp nên ổn, nhưng với số lẻ nên soMath.abs(delta) < 1e-9.
Tự luyện
- Thêm
inNghiemDep(): nghiệm nguyên thì in không có.0. - Đếm trong 10 bộ hệ số có bao nhiêu bộ cho 2 nghiệm phân biệt.
Bài 4. NhanVien — đọc công thức từ code
Đề nói gì
Mỗi nhân viên có tên, ngày sinh, chức vụ, hệ số, lương cơ bản. Tính lương, BHXH 6%, BHTN 1%, thực lĩnh, tuổi; tìm người lớn tuổi nhất, tổng và trung bình lương.
Từ mới
getYear() + 1900: thư việnDatecũ lưu năm = năm thực − 1900, muốn năm thật phải cộng lại. Bẫy kinh điển, nhớ công thức.Math.round(...): làm tròn để in cho đẹp (tiền lẻ đồng nhìn rối).
Ý tưởng
phuCap()theo chức vụ bằngif-elsevới.equals.luong = (heSo + phuCap) * luongCoBan.- Các khoản = tỉ lệ × lương; thực lĩnh = lương − 2 khoản.
- Duyệt tìm max tuổi, cộng tổng.
Code mổ xẻ
public double phuCap() {
if (chucVu.equals("GD")) return 1.0; // so chuỗi bằng equals, KHÔNG ==
if (chucVu.equals("PGD")) return 0.8;
...
return 0; // chức vụ lạ: không phụ cấp
}
public double luong() { return (heSoLuong + phuCap()) * luongCoBan; }
public double thucLinh() { return luong() - bhxh() - bhtn(); }
NhanVien gia = ds[0]; // giữ người lớn tuổi nhất
for (int i = 1; i < ds.length; i++)
if (ds[i].tuoi() > gia.tuoi()) gia = ds[i]; // gặp người hơn tuổi thì đổi
Mẫu "giữ tạm rồi đổi khi gặp đứa hơn" (giữ max) dùng cho mọi bài tìm
lớn nhất/nhỏ nhất. Vòng lặp bắt đầu từ 1 vì ds[0] đã giữ sẵn.
Chạy tay
Nguyễn Văn A (GD, 5.0, 1500000): (5.0+1.0)*1500000 = 9000000,
BHXH 540000, BHTN 90000, thực lĩnh 8370000. Khớp output mẫu.
A sinh 1960 → 66 tuổi, lớn nhất đám. Tổng lương 5 người 25180000,
trung bình 5036000.
Bẫy hay gặp
chucVu == "GD": so địa chỉ ô nhớ, chuỗi nhập vào khác ô nhớ hằng là sai dù chữ giống hệt. Chuỗi luôn.equals.getYear()quên+ 1900: tuổi ra số âm mấy chục năm.
Tự luyện
- Thêm thưởng Tết = 1 tháng lương cho người làm trên 10 năm
(2026 − năm vào làm, thêm thuộc tính
namVaoLam). - Liệt kê người thực lĩnh trên 5 triệu.
Phụ lục OOP: object nói chuyện với object
Sơ đồ object trong bài phân số
PS p = new PS(1, 2) đúc object có tu=1, mau=2.
p.cong(q) là gửi thông điệp: "p ơi, cộng với q giúp tôi",
p tự đọc tu/mau của mình (this) và của q, đúc object mới trả về.
Object cũ không đổi — phép toán thuần khiết, không tác dụng phụ.
Ngược lại r.rutGon() là thông điệp "tự sửa mình đi" (void).
Dây chuyền gọi hàm trong ATM
chuyenTien không làm trực tiếp mà nhờ 2 object khác việc:
ds[0].chuyenTien(ds[1], 500)
// bên trong: this(=ds[0]).rutTien(500) → ds[1].napTien(500)
Một lời gọi ngoài mặt, ba object tham gia: người chuyển (this),
người nhận (tk), và luật nằm gọn trong rutTien.
Muốn đổi luật (thêm phí) chỉ sửa rutTien — chuyenTien hưởng theo.
Đây là ủy thác (delegation): object nhờ object khác làm việc mình không cần biết chi tiết.
Đóng gói nhìn từ NhanVien
heSoLuong, luongCoBan đều private. Code ngoài không thể
nv.luongCoBan = -5 để phá. Mọi đường vào đều qua constructor
và getter; công thức lương nằm một chỗ trong luong(),
bhxh()/bhtn()/thucLinh() dùng lại nó thay vì tính riêng —
đổi công thức một chỗ, cả bảng lương đúng theo.
IS-A đầu tiên (đọc trước Lab 05)
PTB2, ATM, NhanVien chưa kế thừa ai — chúng là lớp độc lập.
Sang Lab 05, HPLyThuyet là một DiemHP (IS-A) nên mới
đựng chung mảng cha được. Nhớ cảm giác PS[], ATM[], NhanVien[]
chỉ đựng một loại — Lab 05 phá giới hạn đó.