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

Ý tưởng

  1. Dựng object, chặn mẫu 0, chuẩn hóa dấu.
  2. Rút gọn bằng Euclid (lấy dư liên tục).
  3. Cộng/trừ/nhân/chia theo công thức phân số đã học ở phổ thông.
  4. 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

Tự luyện

  1. Thêm luyThua(int k) trả về phân số mũ k.
  2. Thêm honSo(): 7/3 in ra 2(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

Ý tưởng

  1. Constructor ép số dư tối thiểu.
  2. napTien: tiền dương mới cộng.
  3. rutTien: tiền sai hoặc quá số dư → false; đủ → trừ, true.
  4. 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

Tự luyện

  1. Thêm phí rút 1000đ mỗi lần (trừ vào số dư, vẫn phải đủ số dư).
  2. 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

Ý tưởng (viết ra giấy trước khi code)

  1. a == 0: b == 0 nữa thì c == 0 vô số nghiệm, ngược lại vô nghiệm; b khác 0 thì x = -c/b.
  2. a khá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

Tự luyện

  1. Thêm inNghiemDep(): nghiệm nguyên thì in không có .0.
  2. Đế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

Ý tưởng

  1. phuCap() theo chức vụ bằng if-else với .equals.
  2. luong = (heSo + phuCap) * luongCoBan.
  3. Các khoản = tỉ lệ × lương; thực lĩnh = lương − 2 khoản.
  4. 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

Tự luyện

  1. 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).
  2. 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 đó.