عرض عملي: الحسابات المصرفية v1 من ملف فارغ
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
This is a follow-along lesson. You will build the lab's Account class and a small Main from an empty file, one small step at a time, using everything from Lessons 1 to 5. At the end you will have a working program, and a clear idea of what the lab (BEG-020-LAB1) expects from you.
The lesson also contains a planned mistake. In step 5 we will write owner = owner; on purpose, see null in the output, and fix it. Seeing a bug appear and disappear is a better lesson than being told about it.
Why this matters: a bank account with an owner, a number, and a balance, with deposit and withdraw rules, is the smallest realistic example of a backend domain class. The same shape repeats for orders, users, and products.
This lesson builds on all of Lessons 1 to 5. Menus differ between IDEs and operating systems, so we describe actions generically ("create a new file") and show the command line where useful.
Learning Objectives
By the end of this lesson you will be able to:
- Build a class from an empty file in the order: fields, constructors, methods.
- Write a full constructor and an overloaded one that forwards with
this(...). - Write
depositandwithdrawmethods that protect the balance. - Keep
mainto wiring only: create objects, call methods, print. - Recognise and fix the missing-
thisbug from its symptom. - Check your work against the lab's acceptance criteria.
Written Explanation
The plan
The lab asks for an Account with an owner, a number, and a balance; a full constructor and an overloaded one; deposit and withdraw; several accounts created in main; and a static counter of accounts. The acceptance criteria are: it compiles, it works, and main contains no logic beyond wiring. "Wiring" means creating objects, calling their methods, and printing. Any rule about money, such as "you cannot withdraw more than you have", belongs in Account, not in main.
We build in this order, compiling after each step:
- Fields.
- The full constructor.
- The overloaded constructor.
depositandwithdraw.- A planned mistake and its fix.
mainas wiring.
Working in small steps matters. If the code stops compiling after a change, you know that the last few lines caused it.
Step 1: the fields
Create a new file named Account.java. (In an IDE, use the "new class" action and type the name; on the command line, create the file in an empty folder.) Write:
public class Account {
String owner;
String number;
double balance;
}The file name matches the class name, and the fields sit outside any method. Compile it (javac Account.java on the command line, or your IDE's build action). Nothing runs yet, but the compiler accepts it.
Step 2: the full constructor
Add the constructor that sets all three fields:
Account(String owner, String number, double balance) {
this.owner = owner;
this.number = number;
this.balance = balance;
}Every parameter has the same name as its field, and every assignment starts with this.. This is the pattern from Lessons 2 and 3.
Step 3: the overloaded constructor
A new account often starts empty. Add a second constructor that forwards to the first:
Account(String owner, String number) {
this(owner, number, 0.0);
}No assignments are repeated. Only one constructor, the full one, sets fields.
Step 4: deposit and withdraw
The methods hold the rules:
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
boolean withdraw(double amount) {
if (amount <= 0 || amount > balance) {
return false;
}
balance -= amount;
return true;
}deposit ignores zero and negative amounts. withdraw refuses nonsense and overdrafts, changes the balance only when it can, and tells the caller whether it worked. Add describe() from Lesson 3, which returns owner + " (" + number + "): " + balance, so that main has something to print.
Step 5: the planned mistake
Now suppose that, while editing the full constructor, you slip and write:
Account(String owner, String number, double balance) {
owner = owner; // mistake: the parameter is assigned to itself
this.number = number;
this.balance = balance;
}It compiles. When you run the wiring from step 6, Ana's line shows:
null (A-1): 150.0The owner has become null, while the number and balance, which kept this., are fine. This is exactly the symptom from Lesson 3: one field is at its default although a value was passed in. Compare the three assignments: only the one without this. is wrong. Restore this.owner = owner;, run again, and the owner returns.
A tip for finding this bug fast: when a single field is wrong and the others are right, compare its assignment line with the lines that work.
Step 6: main as wiring
Create a second file, Main.java:
public class Main {
public static void main(String[] args) {
Account ana = new Account("Ana", "A-1", 100);
Account omar = new Account("Omar", "A-2");
ana.deposit(50);
omar.deposit(20);
boolean ok = ana.withdraw(500);
System.out.println(ana.describe());
System.out.println(omar.describe());
System.out.println("Second withdrawal ok? " + ok);
}
}Look at what main does: create two accounts, call methods, print. It has no if, no arithmetic on balances, and no rules. When a requirement changes, only Account changes.
Compile and run both files together (javac Account.java Main.java, then java Main, or use your IDE's run action on Main). The output on JDK 21:
Ana (A-1): 150.0
Omar (A-2): 20.0
Second withdrawal ok? falseAna started with 100 and received 50. Her attempt to withdraw 500 was refused, so her balance stayed at 150. Omar started with 0 through the overloaded constructor and received 20.
Adding the counter
The lab also asks for a static counter of accounts created. Lesson 5 shows how: add static int count = 0; to Account and count++; to the full constructor, so the overloaded constructor counts too through this(...). Then a final line in main, System.out.println("Accounts created: " + Account.count);, prints:
Accounts created: 2Because the short constructor forwards to the full one, the counting line exists in exactly one place. If you had copied the assignments into both constructors, you would have to remember to count in both.
Examples
Example 1: the finished Account (simple)
public class Account {
static int count = 0;
String owner;
String number;
double balance;
Account(String owner, String number, double balance) {
this.owner = owner;
this.number = number;
this.balance = balance;
count++;
}
Account(String owner, String number) {
this(owner, number, 0.0);
}
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
boolean withdraw(double amount) {
if (amount <= 0 || amount > balance) {
return false;
}
balance -= amount;
return true;
}
String describe() {
return owner + " (" + number + "): " + balance;
}
}This is the whole class: one static field, three instance fields, two constructors, three methods. You can compare your own version with it, line by line.
Example 2: a method that talks to another account (practical)
A transfer touches two accounts. It still belongs in Account, because it is a rule about money. It can reuse withdraw and deposit, which keeps the checks in one place:
boolean transferTo(Account other, double amount) {
if (this.withdraw(amount)) {
other.deposit(amount);
return true;
}
return false;
}Account a = new Account("Ana", "A-1", 100);
Account b = new Account("Omar", "A-2");
System.out.println(a.transferTo(b, 30) + " " + a.describe() + " " + b.describe());
System.out.println(a.transferTo(b, 500) + " " + a.balance + " " + b.balance);Output:
true Ana (A-1): 70.0 Omar (A-2): 30.0
false 70.0 30.0The first transfer works; the second asks for more than Ana has, so withdraw refuses and nothing moves. this is the sending account, other the receiving one, and main has no logic.
Example 3: a main with too much logic (realistic)
Compare the following main, which a beginner might write:
if (ana.balance >= 500) {
ana.balance = ana.balance - 500;
} else {
System.out.println("Not enough");
}It works today, but the rule "no overdraft" now lives in main. A second program that uses Account has to copy it, and one forgotten check puts an account into a negative balance. The corrected form is a single call, ana.withdraw(500), plus a print on false. This is the acceptance criterion "no logic in main beyond wiring" in practice.
Backend Insight
A backend application is layered, and each layer has a narrow job. The outermost layer, such as a controller that receives an HTTP request, only translates the request into calls: it creates or finds objects and calls methods, just as main does here. The rules live in the domain classes like Account, and the same withdraw rule applies whether the request comes from a web page, a phone app, or a nightly job.
Later modules will move main's job to frameworks, but the discipline is the same one you practised today: thin edges, rules inside the objects. When you review other people's code, this is one of the quickest quality checks: is the business rule in the class where the data lives, or scattered around callers?
Common Mistakes
Mistake 1: Putting rules in main
If main checks balances, you have a procedural program with objects as storage. Move the check into the method of the class that owns the data.
Mistake 2: Missing this. in the constructor
The classic: owner = owner;. Symptom: one field at its default. Compare the failing assignment with a working one.
Mistake 3: Repeating assignments across constructors
Copying all three assignments into the overloaded constructor works, but any later change has to be made twice. Use this(...).
Mistake 4: Forgetting to compile both files
If Main fails with "cannot find symbol" for Account, the compiler did not see Account.java. Compile both files together, or let the IDE build the whole project.
Mistake 5: Using the result of withdraw as if it always succeeded
Ignoring the boolean means the program reports success when nothing happened. Use the result, even if only to print a message.
Exercises
Try each exercise yourself before opening the solutions. Build on your own Account from this lesson.
Level A: Foundation
A1. Why does the overloaded constructor start with this(owner, number, 0.0); instead of repeating the three assignments?
أظهر الحل A1
A1. So that only the full constructor sets fields. If the rules for setting fields change, there is one place to edit, and no risk that two constructors drift apart.
A2. In the finished program, why does Account.count print 2 after two new calls, even though one of the constructors never mentions count?
أظهر الحل A2
A2. Because the short constructor calls the full one through this(...), and the full one executes count++. Every new ends up there.
A3. Which file contains the rule that an account cannot be overdrawn: Account.java or Main.java? Why?
أظهر الحل A3
A3. Account.java. The class owns the balance, so it enforces the rule; any caller then gets the same behavior and cannot forget the check.
Level B: Application
B1. Add a method boolean isEmpty() that returns true when the balance is 0. Use it in main to print whether Omar's new account is empty.
أظهر الحل B1
B1.
boolean isEmpty() {
return balance == 0;
}System.out.println(omar.isEmpty()); // false after depositing 20; true beforeB2. Your teammate's output shows Ana (null): 150.0 for an account created with new Account("Ana", "A-1", 100). Which line do you look for first, and how would you fix it?
أظهر الحل B2
B2. The number field was not assigned: look at the constructor line for it, which probably reads number = number; without this.. Fix: this.number = number;. (Also check the call order of arguments: a swapped call would show a wrong, but not null, value.)
B3. In main, create three accounts, deposit different amounts into each, withdraw too much from one of them, and print all three descriptions. Check that main contains no if.
أظهر الحل B3
B3.
Account a = new Account("Ana", "A-1", 100);
Account b = new Account("Omar", "A-2");
Account c = new Account("Sara", "A-3", 7);
a.deposit(10);
b.deposit(20);
c.deposit(30);
c.withdraw(1000);
System.out.println(a.describe());
System.out.println(b.describe());
System.out.println(c.describe());It prints balances 110.0, 20.0, and 37.0; the oversized withdrawal is refused inside withdraw.
Level C: Challenge
C1. Add transferTo(Account other, double amount) as in Example 2. Then show that a refused transfer leaves both balances unchanged.
أظهر الحل C1
C1. See Example 2. After the refused second call, a.balance and b.balance are still 70.0 and 30.0 because withdraw returned false before deposit ran.
C2. Extend the program with a static method Account.isValidNumber(String number) that returns true when the number starts with "A-", and make the full constructor throw IllegalArgumentException for an invalid number. Explain why the method is static but the check itself runs in the constructor.
أظهر الحل C2
C2.
static boolean isValidNumber(String number) {
return number != null && number.startsWith("A-");
}and, at the top of the full constructor:
if (!isValidNumber(number)) {
throw new IllegalArgumentException("invalid account number: " + number);
}The check needs no particular account, only the string, so it can be static and used even before an account exists. The decision to reject is made when an object is being created, so the constructor calls it. (Exceptions are a preview here; a later module teaches them.)
Mini Challenge
Do the lab's core in your own words, without looking at Example 1:
- Write
Accountwith owner, number, balance, a full constructor, an overloaded constructor,deposit,withdraw, anddescribe. - Write
Mainthat creates at least three accounts, moves money between them with the methods, and prints the result. - Add the static counter and print it at the end.
- Re-read
mainand delete any line that is not creating, calling, or printing.
Check your work: it compiles; every printed value matches what you calculated by hand; main has no business rules.
Summary & Next Lesson
Key takeaways
- Build a class in small steps and compile after each: fields, constructors, methods, then
main. - The full constructor sets the fields; short constructors forward to it with
this(...). - Rules (no overdraft, positive deposits) belong in the class that owns the data;
mainonly wires. - A single field at its default value, while the others are right, usually means a missing
this.. - The static counter increments in the full constructor, so every
newis counted once.
Next lesson
The next lesson, "Demo: static vs instance, seen in the debugger", uses a debugger to show where instance data and static data live, and what happens when balance is made static by mistake.
Self-check
- What are the lab's acceptance criteria?
- Which constructor does the real work, and how do the others use it?
- Which symptom points to a missing
this.? - Where should the rule "no overdraft" live?
- What may
maincontain in this lab?
أظهر الإجابات
- It compiles, it works, and
mainhas no logic beyond wiring. - The full constructor; the others call it with
this(...). - One field is at its default value (
null,0) although a value was passed, while the others are correct. - In
Account, insidewithdraw. - Creating objects, calling their methods, and printing.
References
- Oracle, The Java Tutorials, Providing Constructors for Your Classes. Supports: constructors use the class name and have no return type, and overloading by parameter list.
- Oracle, The Java Tutorials, Using the this Keyword. Supports:
this.field = parameterto avoid shadowing, andthis(...)as the first line of a constructor. - All program output above was produced by compiling and running the code on JDK 21 (javac 21.0.10). The layering comment in Backend Insight is general engineering practice, not tied to a specific source.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.