🏧 BÀI 2: LỚP CÂY ATM - QUẢN LÝ TÀI KHOẢN VÀ THAO TÁC LIÊN KẾT ĐỐI TƯỢNG
1. Trực quan hóa tư duy OOP: Tương tác giữa các Object
Trong bài Phân số, ta đã thấy một phân số này thực hiện phép tính với một phân số khác. Ở bài ATM này, tư duy OOP thể hiện rất rõ ở hành động Chuyển tiền:
Bạn mang tài khoản của mình (TK A) đến cây ATM.
Bạn nhập số tài khoản người nhận (TK B) và số tiền cần chuyển.
Cây ATM sẽ thực hiện: Trừ tiền ở TK A và Cộng tiền vào TK B. Đây chính là sự tương tác trực tiếp giữa 2 đối tượng (Object) thuộc cùng một lớp ATM.
2. Đọc hiểu sơ đồ UML của lớp ATM
Dựa trên đề bài Lab 02, sơ đồ lớp ATM như sau:
| Thuộc tính / Phương thức | Cú pháp trong Java | Ý nghĩa |
|---|---|---|
| soTK: String | private String soTK; | Số tài khoản ngân hàng (dạng chuỗi chữ/số) |
| soDu: double | private double soDu; | Số dư tiền hiện có trong tài khoản |
| + ATM(stk, sd) | public ATM(String stk, double sd) | Hàm khởi tạo tài khoản với số TK và số dư ban đầu |
| + laySTK(): String | public String laySTK() | Getter lấy ra số tài khoản |
| + laySoDu(): double | public double laySoDu() | Getter lấy ra số dư hiện tại |
| + toString(): String | public String toString() | Chuyển thông tin tài khoản thành chuỗi để in |
| + napTien(st): void | public void napTien(double st) | Nạp thêm số tiền st vào tài khoản |
| + rutTien(st): boolean | public boolean rutTien(double st) | Rút số tiền st. Trả về true nếu thành công, false nếu thất bại |
| + chuyenTien(ATM tk, double st): boolean | public boolean chuyenTien(ATM tk, double st) | Chuyển tiền sang đối tượng tk khác |
3. Phân tích & Cài đặt Mã nguồn Java Chi Tiết
Bước 3.1: Tạo lớp ATM.java
public class ATM {
// 1. Thuộc tính đóng gói
private String soTK;
private double soDu;
// 2. Phương thức khởi tạo (Constructor)
public ATM(String soTK, double soDu) {
this.soTK = soTK;
// Kiểm tra điều kiện số dư không được âm khi khởi tạo
if (soDu < 0) {
System.out.println("Lỗi: Số dư ban đầu không được âm! Đặt mặc định = 0.");
this.soDu = 0;
} else {
this.soDu = soDu;
}
}
// 3. Các hàm Getter
public String laySTK() {
return this.soTK;
}
public double laySoDu() {
return this.soDu;
}
// 4. Hàm nạp tiền
public void napTien(double st) {
if (st <= 0) {
System.out.println("Lỗi: Số tiền nạp phải lớn hơn 0!");
return;
}
this.soDu += st; // Cộng trực tiếp vào số dư hiện tại
System.out.println("Nạp thành công " + st + " vào TK " + this.soTK);
}
// 5. Hàm rút tiền
public boolean rutTien(double st) {
if (st <= 0) {
System.out.println("Lỗi: Số tiền rút phải lớn hơn 0!");
return false;
}
// Kiểm tra điều kiện đủ tiền để rút
if (st > this.soDu) {
System.out.println("Rút tiền thất bại: Số dư TK " + this.soTK + " không đủ!");
return false;
}
this.soDu -= st; // Trừ tiền khỏi số dư
System.out.println("Rút thành công " + st + " từ TK " + this.soTK);
return true;
}
// 6. Hàm chuyển tiền sang tài khoản khác (Đối tượng ATM khác)
public boolean chuyenTien(ATM tkNhan, double st) {
if (tkNhan == null) {
System.out.println("Lỗi: Tài khoản nhận không tồn tại!");
return false;
}
// Gọi hàm rutTien từ chính đối tượng hiện tại (this)
// Nếu rút tiền thành công thì mới nạp tiền vào tài khoản nhận
if (this.rutTien(st)) {
tkNhan.napTien(st);
System.out.println("Chuyển tiền thành công từ TK " + this.soTK + " sang TK " + tkNhan.laySTK());
return true;
}
System.out.println("Chuyển tiền thất bại!");
return false;
}
// 7. Hàm chuyển đối tượng thành dạng String biểu diễn thông tin
@Override
public String toString() {
return "STK: " + this.soTK + " | Số dư: " + this.soDu;
}
}
Bước 3.2: Viết chương trình chính MainATM.java (Thực thi các yêu cầu Lab 02)
public class MainATM {
public static void main(String[] args) {
// 1. Tạo danh sách 5 tài khoản, mỗi tài khoản có sẵn số tiền tối thiểu là 100
ATM[] dsTK = new ATM[] {
new ATM("1111111", 100),
new ATM("2222222", 100),
new ATM("3333333", 100),
new ATM("4444444", 100),
new ATM("5555555", 100)
};
System.out.println("=== DANH SÁCH TÀI KHOẢN BAN ĐẦU ===");
inDanhSach(dsTK);
System.out.println("\n=== THỰC HIỆN CÁC THAO TÁC BÀI TẬP ===");
// 2. Tài khoản 1111111 nạp tiền vào tài khoản 1000
dsTK[0].napTien(1000);
// 3. Tài khoản 1111111 chuyển cho tài khoản 2222222 số tiền 500
dsTK[0].chuyenTien(dsTK[1], 500);
// 4. Tài khoản 2222222 rút số tiền 200
dsTK[1].rutTien(200);
// 5. In số tiền hiện có của các tài khoản
System.out.println("\n=== SỐ TIỀN HIỆN CÓ CỦA CÁC TÀI KHOẢN ===");
inDanhSach(dsTK);
// 6. In tổng số tiền của các tài khoản
double tongTien = 0;
for (ATM tk : dsTK) {
tongTien += tk.laySoDu();
}
System.out.println("\n=> TỔNG SỐ TIỀN CỦA TOÀN BỘ HỆ THỐNG: " + tongTien);
}
// Hàm phụ trợ để in danh sách tài khoản cho ngắn gọn
public static void inDanhSach(ATM[] ds) {
for (ATM tk : ds) {
System.out.println(tk);
}
}
}
⚠️ Lỗi sai phổ biến người mới bắt đầu cần tránh!
Lỗi viết lặp lại code (Redundancy):
Mắc phải: Khi viết hàm chuyenTien(ATM tk, double st), người mới hay tự viết lại logic trừ tiền của tài khoản gửi và cộng tiền cho tài khoản nhận.
Lời khuyên OOP: Hãy tái sử dụng các phương thức đã có (rutTien và napTien). Giúp code gọn gàng và nếu sau này logic rút tiền thay đổi (ví dụ: mất thêm phí giao dịch 1100đ), bạn chỉ cần sửa ở 1 nơi.
Lỗi NullPointer khi chuyển tiền:
Mắc phải: Truyền một đối tượng null vào tham số chuyenTien(tk, st) mà không kiểm tra if (tk == null), dẫn đến ứng dụng bị crash.
📝 BÀI TẬP TỰ RÈN LỰC
Đề bài: Bổ sung phương thức public boolean rutTienCoPhi(double st, double phi) vào lớp ATM.
Hàm thực hiện rút số tiền st kèm theo khoản phí phi.
Gợi ý: Điều kiện rút tiền công khai lúc này phải là (st + phi) <= soDu.
public boolean rutTienCoPhi(double st, double phi) {
double tongTienPhaiTra = st + phi;
if (tongTienPhaiTra <= 0) {
System.out.println("Lỗi: Số tiền rút không hợp lệ!");
return false;
}
if (tongTienPhaiTra > this.soDu) {
System.out.println("Không đủ số dư để rút " + st + " cùng với phí " + phi);
return false;
}
this.soDu -= tongTienPhaiTra;
System.out.println("Rút thành công " + st + " (Phí: " + phi + "). Số dư còn lại: " + this.soDu);
return true;
}
Bổ sung: hiểu sâu từng dòng (Bài 2)
Từ mới trong bài này (giải từ số 0):
- Tương tác giữa các object: một object gọi phương thức của object khác.
dsTK[0].chuyenTien(dsTK[1], 500)chính là "tài khoản A nhờ/và tác động lên tài khoản B". - boolean: kiểu đúng/sai.
rutTientrả vềtruenếu rút được,falsenếu thất bại — nơi gọi dựa vào đó để quyết định bước tiếp theo. - null: "không trỏ vào object nào cả". Gọi phương thức trên null là crash ngay (NullPointerException), nên
chuyenTienkiểm tratkNhan == nulltrước tiên. - Tái sử dụng phương thức: thay vì viết lại logic trừ/cộng tiền trong
chuyenTien, ta gọi lạirutTienvànapTienđã có. Sửa luật ở một nơi, cả hai cùng đúng. - @Override + toString():
toString()là hàm Java cho sẵn ở mọi object; viết lại (ghi đè) nó đểSystem.out.println(tk)in ra "STK: ... | Số dư: ..." thay vì dãy ký tự vô nghĩa.
Mổ xẻ hàm chuyển tiền (dòng chảy 4 bước):
public boolean chuyenTien(ATM tkNhan, double st) {
if (tkNhan == null) { ... return false; } // bước 0: chặn crash
if (this.rutTien(st)) { // bước 1: trừ tiền mình; rutTien đã kiểm tra st > 0 và đủ số dư
tkNhan.napTien(st); // bước 2: chỉ khi trừ thành công mới cộng cho người nhận
... return true; // bước 3: báo thành công
}
... return false; // trừ thất bại thì không cộng, tiền không tự sinh ra hay mất đi
}
Điểm tinh tế: nếu rutTien thất bại (thiếu tiền), napTien không bao giờ chạy — tổng tiền hệ thống được bảo toàn. Đó là lý do output mẫu: 1111111 còn 600, 2222222 thành 400, tổng vẫn 1300.
Trace tay: 5 tài khoản, mỗi cái 100. Nạp 1000 vào 1111111 → 1100. Chuyển 500 sang 2222222 → 600 và 600. Rút 200 ở 2222222 → 400. Tổng = 600 + 400 + 100 + 100 + 100 = 1300. khớp output.
Tự kiểm tra:
- Vì sao
chuyenTienkhông tự viếtthis.soDu -= st; tkNhan.soDu += st;?
Đáp án
VìsoDu là private của object khác — Java cho phép cùng lớp truy cập nhau, nhưng viết thế sẽ bỏ qua mọi kiểm tra (số âm, thiếu tiền, null). Gọi lại rutTien/napTien thì được kiểm tra miễn phí.rutTien(0)hoặcrutTien(-50)trả về gì?
Đáp án
false, kèm thông báo lỗi — vì điều kiện st <= 0 chặn ngay từ đầu, số dư không đổi.- Quên kiểm tra
tkNhan == nullthì sao?
Đáp án
DòngtkNhan.napTien(st) sẽ ném NullPointerException và cả chương trình crash.