Skip to content
Ameen Abozaid
PublishedBEG-022-L06

Demo: savings and checking accounts with extends

  • Lesson 6 of 7
  • 11 min read
  • Beginner
On This Lesson 11 topics

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() with super.describe().
  • Cause, read, and fix the missing-super(...) compile error on purpose.
  • Trace a main program 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:

  • withdraw returns true or false so callers know whether it worked.
  • A protected method changeBalance lets 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:

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:

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:

  1. The constructor starts with super(owner, number, balance), which builds the Account part first, then it sets the new field.
  2. addInterest() is a new method. It does not touch the balance field; it goes through deposit, so the parent's rule still applies.
  3. describe() is overridden with @Override; it reuses the parent text through super.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:

text
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 error

Read 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:

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:

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:

text
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

MemberSavingsAccountCheckingAccount
getOwner, getNumber, getBalance, depositinheritedinherited
withdrawinheritedoverridden
describeoverridden (using super)overridden (using super)
addInterest, ratenewnone
overdraftLimitnonenew
owner, number, balance fieldsexist inside the object, not accessible by namesame
Constructorsown, 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:

java
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.

Show solution 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(...)?

Show solution 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?

Show solution 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?

Show solution 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?

Show solution B2

B2. Only SavingsAccount:

java
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.

java
CheckingAccount c = new CheckingAccount("Ali", "C-9", 50, 100);
c.withdraw(120);
c.withdraw(40);
c.deposit(-5);
c.deposit(200);
Show solution 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.

Show solution 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.

Show solution 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.
  • @Override plus super.method() lets a child extend rather than copy the parent's behavior.
  • A protected method 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

  1. Which line of a subclass constructor must come first?
  2. Which methods did CheckingAccount override?
  3. Why does addInterest() call deposit instead of changing the balance directly?
  4. What does super.describe() return?
  5. If Account gains a new constructor parameter, what must every child change?
Show answers
  1. The super(...) call.
  2. withdraw and describe.
  3. The balance is private in the parent, and deposit applies the parent's rules.
  4. The parent's text: the account number, owner, and balance.
  5. 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.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Continue to next lesson

Type to search levels, phases, modules and lessons.