Demo: month-end processing without a single type check
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
instanceofloop into one polymorphic call, following the steps in order. - Predict the wrong output that a misspelled override produces.
- Read the compiler message that
@Overrideproduces 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.
| Step | Action | What you should see |
|---|---|---|
| 1 | Start from a loop with instanceof checks | Interest and fee applied |
| 2 | Add monthEnd() to Account with an empty default | Program still compiles |
| 3 | Override it in SavingsAccount and CheckingAccount | Same results when called |
| 4 | Replace the loop body with a.monthEnd() | Same output as step 1 |
| 5 | Planted mistake: misspell the override | Wrong output, then a compiler error |
| 6 | Add BusinessAccount, run the same loop | Correct 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):
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.
// 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
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:
Ana: 1020.0
Omar: 495.0Each 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:
void monthend() { deposit(getBalance() * rate); } // lowercase e: a different nameThis 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:
Ana: 1000.0
Omar: 495.0Ana 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:
@Override void monthend() { deposit(getBalance() * rate); }Demo.java:15: error: method does not override or implement a method from a supertype
@Override void monthend() { deposit(getBalance() * rate); }
^
1 errorThe 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.
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:
Ana: 1020.0
Omar: 495.0
Shop: 1975.0The 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:
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:
Ana 1020.00
Omar 495.00
Shop 1975.00
Lina 100.00
Zed 65.00
net change: 5.00The 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.
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.
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
@Overridecompiles 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
- Why does a misspelled override compile without
@Override? - What does the loop in the demo call on each account?
- What did
BusinessAccountchange in the loop? - Why is a silent wrong answer worse than a crash?
- How can you check the lab's acceptance criterion from a diff?
Show answers
- The misspelled method is just a new method with a different name, so nothing is wrong as far as the compiler knows.
monthEnd(), declared onAccountand overridden by subclasses.- Nothing. Only the new class and the line that creates the object were added.
- A crash gets noticed and investigated; a wrong number can reach customers unnoticed.
- 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
@Overridedoes 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.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.