Skip to content
Ameen Abozaid
PublishedBEG-023-L05

Demo: month-end processing without a single type check

  • Lesson 5 of 5
  • 11 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

This is a follow-along demo that puts the whole module together. You will start from a month-end loop full of instanceof checks, refactor it into a single polymorphic call, deliberately make the classic override mistake and watch @Override catch it, and finally add a brand-new account type to a loop you do not touch.

The demo is also the rehearsal for this module's lab, "Month-end processing": apply month-end rules to a mixed collection of accounts with a single loop and no type checks, so that adding a new account type needs no change to the loop. If you can follow all six steps here with the code in front of you, the lab is a repeat with your own variations.

Why this matters: the planted mistake in step 5 is the most common real-world way polymorphism fails. The code compiles, runs, and produces a wrong result without any error. Seeing it fail once, safely, is the best protection.

This lesson uses everything from lessons 1 to 4 of the module and the Account, SavingsAccount, and CheckingAccount classes from the previous module. Menus and shortcuts differ between IDEs and operating systems, so the steps below are described by action ("compile and run"), not by clicks. A terminal with javac and java, or any IDE's run button, works equally well.

Learning Objectives

By the end of this lesson you will be able to:

  • Refactor a month-end instanceof loop into one polymorphic call, following the steps in order.
  • Predict the wrong output that a misspelled override produces.
  • Read the compiler message that @Override produces for the same mistake.
  • Add a new subclass and show the loop was not edited.
  • Report the results of a processing run in a readable format.
  • State the lab's acceptance criterion and check it yourself.

Written Explanation

The plan

The demo has six steps. Do them in order, and run the program after each one.

StepActionWhat you should see
1Start from a loop with instanceof checksInterest and fee applied
2Add monthEnd() to Account with an empty defaultProgram still compiles
3Override it in SavingsAccount and CheckingAccountSame results when called
4Replace the loop body with a.monthEnd()Same output as step 1
5Planted mistake: misspell the overrideWrong output, then a compiler error
6Add BusinessAccount, run the same loopCorrect output with no loop edit

Rules for a safe refactoring

  • Keep the old loop until the new one matches. Run both on fresh data and compare. A refactoring changes structure, not behavior.
  • Change one thing at a time. Add the parent method first, then the overrides, then the loop. If something breaks you know which step caused it.
  • Always write @Override. Step 5 demonstrates why.

The starting code

The classes from before (shortened to what the demo needs):

java
class Account {
    private final String owner;
    private double balance;
    Account(String owner, double balance) { this.owner = owner; this.balance = balance; }
    String getOwner() { return owner; }
    double getBalance() { return balance; }
    void deposit(double amount) { balance += amount; }
    void withdraw(double amount) { balance -= amount; }
}
class SavingsAccount extends Account {
    private final double rate;
    SavingsAccount(String owner, double balance, double rate) { super(owner, balance); this.rate = rate; }
    double getRate() { return rate; }
}
class CheckingAccount extends Account {
    private final double fee;
    CheckingAccount(String owner, double balance, double fee) { super(owner, balance); this.fee = fee; }
    double getFee() { return fee; }
}

The step 1 loop applies the rules with type checks, as in lesson 4. After steps 2 to 4 the classes gain the overrides, and the loop shrinks to one call. By then the code matches the month-end example from lesson 1.

Examples

Example 1: steps 1 to 4, the refactoring (simple)

Steps 2 and 3: add the parent default, then the two overrides.

java
// in Account:
void monthEnd() { }                      // default: nothing to do

// in SavingsAccount:
@Override void monthEnd() { deposit(getBalance() * rate); }

// in CheckingAccount:
@Override void monthEnd() { withdraw(fee); }

Step 4: the loop becomes

java
for (Account a : accounts) {
    a.monthEnd();
    System.out.println(a.getOwner() + ": " + a.getBalance());
}

With Ana's savings account (1000.0 at 2%) and Omar's checking account (500.0 with a 5.0 fee), the output is the same as the old chain produced:

text
Ana: 1020.0
Omar: 495.0

Each of the four steps compiles and runs on its own. If your output differs from the old loop's at this point, stop and fix it before going on: do not move to step 5 with a broken refactoring.

Example 2: step 5, the planted mistake (practical)

Now imagine a typing slip in SavingsAccount, written without @Override:

java
void monthend() { deposit(getBalance() * rate); }    // lowercase e: a different name

This compiles. monthend (lowercase e) is simply a brand-new method, unrelated to monthEnd. Nothing calls it. The loop calls monthEnd(), finds the parent's empty default, and does nothing:

text
Ana: 1000.0
Omar: 495.0

Ana received no interest, and there was no error and no exception, only a wrong number. In a real system, this is the kind of bug that reaches customers.

Now add the annotation:

java
@Override void monthend() { deposit(getBalance() * rate); }
text
Demo.java:15: error: method does not override or implement a method from a supertype
    @Override void monthend() { deposit(getBalance() * rate); }
    ^
1 error

The compiler now stops you immediately and points at the exact line. The Java API documentation for @Override says this is what the annotation is for: it is an error if the annotated method does not override a supertype method. Fix the spelling, remove nothing else, and the output is back to Ana: 1020.0.

The same protection covers the other half of the module's common mistakes, confusing overloading with overriding. A method such as void monthEnd(int days) in the subclass would also be rejected by @Override, because it overloads instead of overriding.

Example 3: step 6, a new type and an untouched loop (realistic)

Add a new class in its own place in the code. Do not edit the loop.

java
class BusinessAccount extends Account {
    BusinessAccount(String owner, double balance) { super(owner, balance); }
    @Override void monthEnd() { withdraw(25.0); }    // flat monthly charge
}

Add new BusinessAccount("Shop", 2000.0) to the array of accounts, which is data, not logic. Then run the same loop as before:

text
Ana: 1020.0
Omar: 495.0
Shop: 1975.0

The loop was not edited, and the new type is processed correctly. That is the lab's acceptance criterion, shown to hold.

A useful way to check it for the lab: compare the loop's text before and after adding the class, using your version-control tool or just by eye. If any line of the loop differs, the criterion is not met. The only places that should change are the new class and the line that creates an object of it.

For the lab, you may also report the results more neatly, and compute a total without any type check, using the balance difference rather than any type-specific knowledge:

java
double change = 0;
for (Account a : accounts) {
    double before = a.getBalance();
    a.monthEnd();
    change += a.getBalance() - before;
    System.out.printf("%-5s %8.2f%n", a.getOwner(), a.getBalance());
}
System.out.printf("net change: %.2f%n", change);

With Ana, Omar, Shop, a student account Lina (100.0, which inherits the empty default so no fee is charged), and a Gold account that deposits 15.0 each month, it prints:

text
Ana    1020.00
Omar    495.00
Shop   1975.00
Lina    100.00
Zed      65.00
net change: 5.00

The net change is 20 - 5 - 25 + 0 + 15 = 5, and the loop still contains no type check.

Backend Insight

The planted mistake in this demo is a stand-in for a real-world category: silent failures. A batch job that completes without errors while quietly skipping a rule is far more expensive than one that crashes, because nobody investigates a green dashboard. @Override turns one silent failure into a loud compile-time failure for free, which is why teams often make it part of their code-style rules.

Month-end processing is also a real backend shape: a scheduled job loads a batch of records, applies a rule to each, and reports totals. Keeping the rule inside the record's class means the job itself is short, and a new product (a new account type) arrives as a new class plus a line that creates it, with no edit to code that is already running in production. Notice also the reporting loop above: it needed the balance before and after, which works for every type, rather than any type-specific knowledge.

Common Mistakes

Mistake 1: Leaving out @Override

  • Incorrect: void monthend() { ... } compiles and quietly does nothing useful.
  • Correct: @Override void monthEnd() { ... }, so a misspelling or a wrong parameter list becomes a compile error.

Mistake 2: Refactoring everything in one go

If the loop, the parent, and two subclasses all change at once, a wrong number gives no clue where to look. Change one thing, run, compare, repeat.

Mistake 3: Editing the loop to add a type

If you catch yourself adding else if (a instanceof BusinessAccount) to the loop, the refactoring is incomplete: the behavior should live in BusinessAccount.monthEnd().

Mistake 4: Casting to get at type-specific data in the processing loop

The loop needs only what the parent declares. If you need getRate() for something in the loop, ask whether the work should be a method on the parent instead.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. Pet has String sound() { return "..."; }. Parrot extends Pet declares String sounds() { return "hello"; } (note the extra s), with no @Override. What does Pet p = new Parrot(); System.out.println(p.sound()); print, and why does it compile?

Show solution A1

A1. It prints .... The program compiles because sounds() is a perfectly valid new method (a different name), so nothing is wrong as far as the compiler knows. The call p.sound() uses the parent's version because nothing overrides it. I compiled and ran this.

A2. What single change makes the compiler report the mistake in A1, and what does the error say in general terms?

Show solution A2

A2. Add @Override above sounds(). The compiler then reports that the method does not override or implement a method from a supertype.

A3. In the demo, which step makes the loop free of type checks, and which step keeps the code compiling while you prepare for it?

Show solution A3

A3. Step 4 (replacing the loop body with a.monthEnd()) makes the loop free of type checks. Step 2 (adding the empty default to Account) keeps the code compiling while you add the overrides, because the call a.monthEnd() is valid on an Account reference as soon as the parent declares it.

Level B: Application

B1. Add StudentAccount extends Account for a student whose fee is waived. Write the shortest correct class and say why it needs no monthEnd().

Show solution B1

B1.

java
class StudentAccount extends Account {
    StudentAccount(String owner, double balance) { super(owner, balance); }
}

It inherits Account's empty monthEnd(), which is the correct behavior for "no fee, no interest". I compiled and ran it: a balance of 100.0 stays 100.0.

B2. Add GoldAccount that deposits 15.0 each month. Show the line you must add to the array and the lines of the loop you must change.

Show solution B2

B2.

java
class GoldAccount extends Account {
    GoldAccount(String owner, double balance) { super(owner, balance); }
    @Override void monthEnd() { deposit(15.0); }
}

Add one line creating a GoldAccount to the array. No line of the loop changes.

B3. A student writes @Override void monthEnd(int days) in SavingsAccount and gets a compiler error. Explain the message in your own words and give the fix.

Show solution B3

B3. The message says the method does not override or implement a method from a supertype. monthEnd(int days) has a different parameter list from monthEnd(), so it is an overload, not an override. The fix is to remove the parameter: @Override void monthEnd() { ... }.

Level C: Challenge

C1. Write the reporting loop that prints each owner and balance after month-end and also the net change in the total of all balances, without any type check or cast. For Ana (savings, 1000, 2%), Omar (checking, 500, fee 5), Shop (business, 2000) the net change is what?

Show solution C1

C1. Use the loop from Example 3 (before/after balance difference with printf). For Ana, Omar, and Shop: +20, -5, -25, so the net change is -10.00. I did not run this exact three-account case; the five-account variant in Example 3 was compiled and run, and the arithmetic here follows the same rules.

C2. Design a short checklist to prove the lab's acceptance criterion to a reviewer who reads only your diff.

Show solution C2

C2. Example checklist: (1) the diff shows no edited line inside the loop; (2) the only changed or added lines are the new class and the line that creates an object of it; (3) the new class's override has @Override; (4) output for the old accounts is unchanged and the new account's result is as expected; (5) no instanceof, cast, or type string appears in the loop.

Mini Challenge

Do the lab warm-up with your own twist. Start from your own Account, SavingsAccount, and CheckingAccount. Give Account an empty monthEnd(), override it in the two subclasses, and write a loop over an array of at least four accounts that prints each owner and new balance and the net change in total balance. Then add a third account type with a rule of your own (for example, a bonus if the balance is above a threshold) without editing the loop.

Check your work:

  • The loop has no instanceof, no casts, and no type strings.
  • Every override has @Override.
  • You tried the planted mistake once (a misspelled override without @Override), saw the wrong number, then added the annotation and read the compiler error.
  • Adding the new type changed only the new class and the line that creates it.

Summary & Next Lesson

Key takeaways

  • The refactoring runs in small steps: add the default to the parent, add the overrides, replace the loop body, compare outputs.
  • A misspelled override without @Override compiles and silently runs the parent's default. With @Override, the compiler stops you.
  • A polymorphic loop needs only what the parent declares, so a new subclass needs no loop edit.
  • Reports can be written type-free by using values every account has, such as balance before and after.
  • The lab's acceptance criterion is checkable by reading the diff: no line in the loop should change.

Next lesson

This lesson finishes the Polymorphism module. The next module, "BEG-024", covers interfaces and abstract classes: ways to say "every type must supply this behavior" and have the compiler enforce it, instead of relying on an empty default.

Self-check

  1. Why does a misspelled override compile without @Override?
  2. What does the loop in the demo call on each account?
  3. What did BusinessAccount change in the loop?
  4. Why is a silent wrong answer worse than a crash?
  5. How can you check the lab's acceptance criterion from a diff?
Show answers
  1. The misspelled method is just a new method with a different name, so nothing is wrong as far as the compiler knows.
  2. monthEnd(), declared on Account and overridden by subclasses.
  3. Nothing. Only the new class and the line that creates the object were added.
  4. A crash gets noticed and investigated; a wrong number can reach customers unnoticed.
  5. Confirm that no line inside the loop was modified and that only the new class and its creation line were added.

References

  • Oracle, Java SE 21 API, Override annotation. Supports the rule that the compiler reports an error when a method annotated with @Override does not override or implement a supertype method, which catches typos and signature mismatches.
  • Oracle, Java Tutorials, Polymorphism. Supports the idea that the JVM runs the version belonging to the actual object, which is why the loop in the demo does not need type checks.
  • Oracle, Java Language Specification SE 21, 15.12.4.4 Locate Method to Invoke. Supports the dynamic lookup starting at the run-time class of the target object.

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.

Type to search levels, phases, modules and lessons.