عرض عملي: حسابات التوفير والحسابات الجارية باستخدام extends
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
This is a hands-on lesson. You have learned the pieces separately: extends, super(...), @Override, final, and when to prefer composition. Now you put them together in one small program, step by step, the way the lab asks: two kinds of bank account that share one Account.
Think of the family tree one more time: Account is the parent who holds the shared traits (an owner, a number, a balance, deposits). SavingsAccount and CheckingAccount are two children. Each inherits the family traits and adds its own: interest for one, an overdraft for the other.
Why this matters: this pattern, a base class plus two focused variants, is how you will meet inheritance in real code. Doing it once from an empty folder, including one planned mistake, makes the rules stick.
This lesson builds on every lesson in this module. Menus and file locations depend on your IDE and operating system, so the steps below say what to create, not where to click.
Learning Objectives
By the end of this lesson you will be able to:
- Build a three-class hierarchy (
Account,SavingsAccount,CheckingAccount) from scratch. - Use
super(...)in both subclass constructors. - Add a new method to one child and override an inherited method in the other.
- Extend a parent's
describe()withsuper.describe(). - Cause, read, and fix the missing-
super(...)compile error on purpose. - Trace a
mainprogram on paper and predict its output.
Written Explanation
This lesson is a guided build. Work in a new empty folder of your IDE project (or any folder with a JDK). Type the code rather than pasting it; you will learn the compiler messages faster.
Step 1: the parent, Account
Start from the account you built in the previous module, with private fields and rules inside the class. This version has an account number and two small changes to prepare for children:
withdrawreturnstrueorfalseso callers know whether it worked.- A
protectedmethodchangeBalancelets a child move the balance without the field being exposed (the previous module's rule: keep the field private, give children only what they need).
File Account.java:
public class Account {
private final String owner;
private final String number;
private double balance;
public Account(String owner, String number, double balance) {
this.owner = owner;
this.number = number;
this.balance = balance;
}
public String getOwner() { return owner; }
public String getNumber() { return number; }
public double getBalance() { return balance; }
public void deposit(double amount) {
if (amount <= 0) {
System.out.println("Deposit refused");
return;
}
balance += amount;
}
public boolean withdraw(double amount) {
if (amount <= 0 || amount > balance) {
System.out.println("Withdrawal refused");
return false;
}
balance -= amount;
return true;
}
protected void changeBalance(double delta) {
balance += delta;
}
public String describe() {
return number + " (" + owner + "): " + balance;
}
}Compile it. Nothing runs yet because there is no main.
Step 2: the first child, SavingsAccount
Apply the is-a test aloud: "a savings account is an account." The child adds a rate and an addInterest() method. File SavingsAccount.java:
public class SavingsAccount extends Account {
private final double rate;
public SavingsAccount(String owner, String number, double balance, double rate) {
super(owner, number, balance);
this.rate = rate;
}
public void addInterest() {
deposit(getBalance() * rate);
}
@Override
public String describe() {
return super.describe() + " [savings, rate " + rate + "]";
}
}Three things to see:
- The constructor starts with
super(owner, number, balance), which builds theAccountpart first, then it sets the new field. addInterest()is a new method. It does not touch the balance field; it goes throughdeposit, so the parent's rule still applies.describe()is overridden with@Override; it reuses the parent text throughsuper.describe()and adds a suffix.
Step 3: the planned mistake
Before the second child, break the first on purpose. Delete the line super(owner, number, balance); from the SavingsAccount constructor and compile. You should see:
SavingsAccount.java:4: error: constructor Account in class Account cannot be applied to given types;
public SavingsAccount(String owner, String number, double balance, double rate) {
^
required: String,String,double
found: no arguments
reason: actual and formal argument lists differ in length
1 errorRead it using Lesson 2: the compiler inserted super(), but Account has no constructor without parameters. required: String,String,double tells you exactly what the parent wants. Put the line back as the first statement and the error disappears. Keep this message in mind: it is the one you will see most.
Step 4: the second child, CheckingAccount
"A checking account is an account", with an overdraft: the balance may go below zero, down to a negative limit. That needs a different withdraw, so the child overrides it. File CheckingAccount.java:
public class CheckingAccount extends Account {
private final double overdraftLimit;
public CheckingAccount(String owner, String number, double balance, double overdraftLimit) {
super(owner, number, balance);
this.overdraftLimit = overdraftLimit;
}
@Override
public boolean withdraw(double amount) {
if (amount <= 0 || amount > getBalance() + overdraftLimit) {
System.out.println("Withdrawal refused");
return false;
}
changeBalance(-amount);
return true;
}
@Override
public String describe() {
return super.describe() + " [checking, overdraft " + overdraftLimit + "]";
}
}The override keeps the parent's promises that matter: non-positive amounts are refused, and the balance never goes below -overdraftLimit. It is a bounded, explicit change, like the one discussed in Lesson 3. The access modifier stays public, matching the parent.
Step 5: use both in main
File Main.java:
public class Main {
public static void main(String[] args) {
SavingsAccount sav = new SavingsAccount("Sara", "S-1", 1000, 0.05);
CheckingAccount chk = new CheckingAccount("Omar", "C-1", 100, 200);
sav.deposit(100);
sav.addInterest();
sav.withdraw(2000);
System.out.println(sav.describe());
chk.withdraw(250);
System.out.println(chk.describe());
chk.withdraw(100);
chk.deposit(300);
System.out.println(chk.describe());
}
}Before running, predict the output on paper. Savings: 1000 plus 100 gives 1100; 5 percent interest adds 55, giving 1155; withdrawing 2000 is refused. Checking: 100 minus 250 gives -150 (allowed by the overdraft); withdrawing 100 more would need to reach -250, past the limit of -200, so it is refused; depositing 300 gives 150.
Run it with JDK 21. The output is:
Withdrawal refused
S-1 (Sara): 1155.0 [savings, rate 0.05]
C-1 (Omar): -150.0 [checking, overdraft 200.0]
Withdrawal refused
C-1 (Omar): 150.0 [checking, overdraft 200.0]Match every line with your prediction. The first Withdrawal refused belongs to sav.withdraw(2000): it is printed before describe() is printed, so it comes first.
Step 6: review what each child inherited, added, and overrode
| Member | SavingsAccount | CheckingAccount |
|---|---|---|
getOwner, getNumber, getBalance, deposit | inherited | inherited |
withdraw | inherited | overridden |
describe | overridden (using super) | overridden (using super) |
addInterest, rate | new | none |
overdraftLimit | none | new |
owner, number, balance fields | exist inside the object, not accessible by name | same |
| Constructors | own, calling super(...) | own, calling super(...) |
If a bug is found later in deposit, you fix it in Account and both children are fixed. That is the payoff for the structure.
Examples
Example 1: the smallest hierarchy (simple)
Read only SavingsAccount: its constructor is two lines, addInterest is one line, and it never repeats deposit. The new code is small because the shared code lives in the parent.
Example 2: a new variant in minutes (practical)
Suppose a third kind, a student account, refuses any single withdrawal above a per-withdrawal limit. The child delegates to the parent after its own check:
public class StudentAccount extends Account {
private final double perWithdrawalLimit;
public StudentAccount(String owner, String number, double balance, double perWithdrawalLimit) {
super(owner, number, balance);
this.perWithdrawalLimit = perWithdrawalLimit;
}
@Override
public boolean withdraw(double amount) {
if (amount > perWithdrawalLimit) {
System.out.println("Over the per-withdrawal limit");
return false;
}
return super.withdraw(amount);
}
}Starting with a balance of 300 and a limit of 100, withdraw(150), withdraw(80), and withdraw(250) print Over the per-withdrawal limit twice and leave a balance of 220.0. This override only adds a restriction and then reuses the parent's rules, so it cannot break the parent's promise.
Example 3: the mistake in a realistic place (realistic)
A common version of the error: you add a field to Account later, say a required branch in its constructor, and recompile. Every child that calls super(owner, number, balance) now fails with the same "cannot be applied to given types" message, with required: listing the new parameter. In a team, this is how a change to a parent constructor surfaces in every child: the compiler lists them all, and you update each super(...) call. It is also a hint about coupling: children depend on the parent's constructor, one more reason to keep hierarchies shallow.
Backend Insight
A real banking backend has more variants than this, and they are usually not modeled with deep inheritance. The reason is the cost shown in Example 3: every change to the base class reaches every child. When the number of variants grows, or the differences are about rules (fees, limits, interest) rather than kinds, teams often keep a single account class and hold a rule object in a field, which is composition. The next lesson does that refactoring in a small case.
Still, a base class plus a handful of well-understood children, like this one, is a legitimate and common design. The skill you practice here is checking at each step: does the child keep the parent's promise (non-negative amounts, a bounded balance)? Does it call super(...)? Is @Override present?
Common Mistakes
Mistake 1: Forgetting super(...)
The error you provoked on purpose. Add it as the first line with the parent's required arguments.
Mistake 2: Omitting @Override on withdraw or describe
If you mistype widthdraw, the child would silently keep the parent's behavior. With @Override the compiler stops you.
Mistake 3: Making balance protected for convenience
It would let any child write the field directly and bypass the parent's rules. The protected method changeBalance keeps the field private.
Mistake 4: Duplicating the parent's checks in the child
Copying the amount <= 0 test works today and drifts tomorrow. When you can call super.withdraw(...), as the student account does, do.
Mistake 5: Overriding with weaker access
Parent public, child package-private: compile error. Keep public.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. For CheckingAccount, list one inherited method, one overridden method, and one member the parent does not have.
أظهر الحل A1
A1. Inherited: deposit, getBalance, getOwner, or getNumber. Overridden: withdraw (or describe). Not in the parent: overdraftLimit.
A2. In SavingsAccount's constructor, what happens if this.rate = rate; is written before super(...)?
أظهر الحل A2
A2. It does not compile: super(...) must be the first statement. (Java also needs the parent part built before the child's.)
A3. Which withdraw runs for chk.withdraw(100): Account's or CheckingAccount's?
أظهر الحل A3
A3. CheckingAccount's, because the object is a CheckingAccount, which overrides it.
Level B: Application
B1. Write StudentAccount as in Example 2 (without looking). What does describe() print if you do not override it, for a student account T-1 owned by Lina with 300?
أظهر الحل B1
B1. The class from Example 2. Without a describe() override the inherited text is used: T-1 (Lina): 300.0.
B2. Change SavingsAccount.addInterest() so that no interest is added when the balance is below 100. Which classes do you edit?
أظهر الحل B2
B2. Only SavingsAccount:
public void addInterest() {
if (getBalance() < 100) {
return;
}
deposit(getBalance() * rate);
}Account and CheckingAccount stay untouched.
B3. Trace this and give the final balance and the number of Withdrawal refused lines.
CheckingAccount c = new CheckingAccount("Ali", "C-9", 50, 100);
c.withdraw(120);
c.withdraw(40);
c.deposit(-5);
c.deposit(200);أظهر الحل B3
B3. Start 50 with a limit of 100. withdraw(120): 120 is not more than 50 + 100 = 150, so it is allowed, balance -70. withdraw(40): -70 + 100 = 30, and 40 is more, so Withdrawal refused. deposit(-5): Deposit refused. deposit(200): balance 130. Final balance 130.0, one Withdrawal refused line (plus one Deposit refused line, which is a different message).
Level C: Challenge
C1. Add a monthEnd() method to both children: savings adds interest, checking charges a flat 5 fee but never makes the balance lower than -overdraftLimit. Where does each method live? Is the shared code better placed in the parent? Explain.
أظهر الحل C1
C1. Each child has its own monthEnd(), because the behavior differs: savings calls addInterest(); checking calls changeBalance(-5) after checking getBalance() - 5 >= -overdraftLimit. A shared monthEnd() in Account could be a no-op that children override, but only if there is shared behavior worth sharing; otherwise it is clearer to keep them in the children. (A later module shows how to call both through one loop without knowing the kind.)
C2. Explain why Account.changeBalance is protected and not public.
أظهر الحل C2
C2. Children need it to move the balance, but outsiders must not: public would let any code change the balance without rules, which breaks encapsulation. protected limits it to subclasses and the same package.
Mini Challenge
Add a PremiumSavingsAccount extends SavingsAccount whose addInterest() pays 1 percentage point more than the base rate (call deposit for the extra part). First, write only the constructor and notice what super(...) call it needs. Then draw the ancestor chain. Finally, decide whether you are comfortable with a three-level chain, and say why composition might be better if five more levels were requested.
Check your work: the chain is PremiumSavingsAccount extends SavingsAccount extends Account extends Object, and you explained a cost of deep hierarchies.
Summary & Next Lesson
Key takeaways
- A base class holds the shared fields and rules; each child adds only what is different.
- Every child constructor calls
super(...)first; a missing call produces the "cannot be applied to given types" error. @Overrideplussuper.method()lets a child extend rather than copy the parent's behavior.- A
protectedmethod can give children controlled access while the field stays private. - A child override must keep the parent's promises.
Next lesson
The next lesson, "Demo: refactoring bad inheritance into composition", takes a wrong "is-a" hierarchy and replaces it with a has-a design.
Self-check
- Which line of a subclass constructor must come first?
- Which methods did
CheckingAccountoverride? - Why does
addInterest()calldepositinstead of changing the balance directly? - What does
super.describe()return? - If
Accountgains a new constructor parameter, what must every child change?
أظهر الإجابات
- The
super(...)call. withdrawanddescribe.- The balance is private in the parent, and
depositapplies the parent's rules. - The parent's text: the account number, owner, and balance.
- Its
super(...)call, to pass the new argument.
References
- Oracle, Java Tutorials, Inheritance, Using the Keyword super, and Overriding and Hiding Methods. Together they support the constructor rule, the
super(...)error,@Override, and the access rule for overrides. - All code in this lesson, the planned compile error, and the program output were compiled and run with JDK 21. The bank scenario and numbers are this course's own.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.