The if-else that kept growing
Riya's fest ticketing app charges a convenience fee. Her fee() method has if (type.equals("CARD")), else if (type.equals("UPI")), else if ("WALLET"). When the team adds net banking, three different files need a new else if, and one is missed: net banking users pay the card fee.
Sir, every new payment type means hunting for every if-else in the code.
Let each payment class answer for itself. Payment has a default fee(), and UpiPayment writes its own fee() with the same name and parameters. That's overriding.
And how does Java know which fee() to run?
At run time it looks at the actual object. A UPI object runs the UPI version, even through a Payment variable. Net banking becomes one new class, and no old code changes.
What is method overriding?
Method overriding occurs when a subclass declares an instance method with the same name and the same parameter list (the same signature) as an accessible instance method of its superclass, with a compatible return type. The subclass's version replaces the inherited one for objects of the subclass, and the JVM chooses which version to execute at run time based on the actual class of the object (dynamic method dispatch). Static, private and final methods cannot be overridden.
The parent says "every payment has a fee". Each child says "here's how my fee is calculated". Same question, different answer depending on who you ask.
Words you'll meet in this lesson
The overriding rules
| Signature | Exactly the same name and parameter types. Different parameters = overloading, not overriding. |
|---|---|
| Return type | Same, or a subtype (covariant) for objects. For primitives it must be identical. |
| Access | Same or wider: protected → public is fine; public → package-private is an error ("weaker access privileges"). |
| Checked exceptions | The override may throw fewer or narrower checked exceptions, never new or broader ones. Unchecked ones are free. |
| Can't override | final (error), static (hidden instead), private (not inherited; a same-named method is just a new method) |
| Which version runs | Decided at run time by the object's class, not the variable's type |
Exam tip: for access, remember the order private < default < protected < public. An overriding method can move right (wider) but never left. And constructors are never overridden, because they aren't inherited.
Three ways to override
Replace
The child's version does something entirely different: UPI's fee is just 0.
new logicExtend
Call super.m() and add to it: a manager's salary = employee's + allowance.
Narrow the return
Parent returns Shape, child returns Circle: callers of the child get the exact type.
The rules, compiled
class Document {
protected String name = "doc";
protected String title() { return "Document"; } // will be widened
Document copy() { return new Document(); } // will be narrowed
void print() throws Exception { System.out.println("printing " + name); }
}
class Invoice extends Document {
@Override
public String title() { return "Tax Invoice"; } // protected → public: wider, OK
@Override
Invoice copy() { return new Invoice(); } // covariant: Invoice is a Document
@Override
void print() { System.out.println("printing invoice"); } // throws nothing: narrower, OK
}
public class Main {
public static void main(String[] args) throws Exception {
Invoice inv = new Invoice();
System.out.println(inv.title());
Invoice copy = inv.copy(); // no cast needed
System.out.println(copy.getClass().getSimpleName());
Document d = inv;
d.print(); // the Invoice version runs
}
}The compile errors you'll meet
| Mistake in the child | javac says |
|---|---|
@Override on fees() (typo) or a different parameter type | method does not override or implement a method from a supertype |
Parent public double fee(), child double fee() | attempting to assign weaker access privileges; was public |
Parent returns double, child returns int | return type int is not compatible with double |
Parent throws IOException, child throws Exception | overridden method does not throw Exception |
Parent method is final | overridden method is final |
Parent method is static, child's is not (or the reverse) | overridden method is static / overriding method is static |
The silent bug @Override catches: public boolean equals(Account other) looks like it overrides Object.equals, but the parameter type differs, so it's an overload. Collections like HashSet call equals(Object) and never use yours. With @Override on it, javac reports the mistake immediately. Put @Override on every overriding method.
Writing an override correctly
Start from the parent's method
The parent's version is the default. Any child that doesn't overrideclass Payment { protected final double amount; Payment(double amount) { this.amount = amount; } double fee() { return amount * 0.02; } // default: 2% }fee()simply inherits it.Copy the signature exactly, add @Override
class UpiPayment extends Payment { UpiPayment(double amount) { super(amount); } @Override double fee() { return 0; } }@Overridegenerates no code. It asks javac to check that a matching parent method exists, turning silent mistakes into compile errors.Keep or widen access, keep or narrow the return type
A caller holding a
These rules exist so that a child object can be used anywhere a parent object is expected without surprises (the Liskov substitution principle).Paymentvariable must be able to callfee()and use the result as adouble. So the child may not hide the method behind narrower access or return something incompatible.Don't add checked exceptions
A caller of
The same reasoning: substitutability. Unchecked exceptions (Payment.fee()handles only the exceptionsPayment.fee()declares. If an override could throw a new checked exception, that caller would be unprepared.RuntimeExceptionand its children) aren't checked by javac, so they're allowed (Module 9).Let the JVM choose at run time
javac compilesPayment p = new UpiPayment(1000); System.out.println(p.fee()); // 0.0, not 20.0p.fee()as aninvokevirtualinstruction. At run time the JVM looks at the object's class (UpiPayment) and runs the most specificfee()it finds there, walking up to parents only if needed. Lesson 7.5 goes deeper.
The object decides which fee() runs: both views
Left: the call and its result. Right: the variable, the object, and where the JVM finds the method.
fee() 6 steps- Default: a plain
Paymentuses the parent'sfee(): 2% of Rs 1,000 = 20.0. - Override: the variable is still a
Payment, but the object is aCardPayment, so its version runs: 18.0. - Another override: a
UpiPaymentobject returns 0.0. - @Override: a misspelt
fees()would silently be a new method. With@Override, javac refuses it. - Static hiding:
info()is static in both classes.p.info()uses the variable's type, Payment, even though the object is UPI. - The payoff: one loop over a
Payment[]gets the right fee from each object. Wallet didn't override, so it inherits 2%.
- The call
p.fee()followspto the object, the object to its class (Payment), and runs that class'sfee(). - Same variable type, but now the object's class is CardPayment, which has its own
fee(). The JVM uses it. - For UpiPayment the search also stops at the object's own class. The variable's declared type doesn't matter for instance methods.
@Overrideis a compile-time check. A misspelt name never reaches the JVM.staticmethods belong to classes, not objects. javac decides from the variable's type (Payment) and the object is never consulted. That's hiding, not overriding.- For each element the JVM starts at the object's class. WalletPayment has no
fee(), so the search moves up to Payment's.
Overriding in real systems
Overriding is how frameworks let your code plug in: they call a method, and your class's version runs.
Watch it with an example: month-end interest at a bank
One month-end job calls a.interest() on every account. Savings and fixed deposits override the method; a current account doesn't. Watch which version runs for each object.
- Setup: the month-end job holds every account in a variable of type
Account. Here it holds a Savings account with Rs 1,00,000. - Override runs:
a.interest()runs Savings' version, chosen by the object's class: 1,00,000 × 3.5 / 100 = 3,500. - Another object: the same variable now holds a FixedDeposit. The same call runs FixedDeposit's override: 7,000.
- No override: Current doesn't declare
interest(), so the JVM searches up to Account and uses its default of 0. - @Override catches typos: a misspelt
intrest()would silently be a new method and customers would get Account's 0%. With@Override, javac refuses to compile it. - The payoff: one loop adds 3,500 + 7,000 + 0. Adding a senior-citizen account later means one new class with its own override; the job doesn't change.
1. toString, equals and hashCode
Almost every domain class overrides toString() for readable logs, and equals()/hashCode() so collections compare by value (Lesson 10.3).
2. Payment and pricing rules
Checkout code calls method.fee(amount) or offer.discount(cart). Each payment method or offer class overrides the calculation; checkout never changes when a new one is added.
3. Thread's run() and other callbacks
Extending Thread and overriding run() is the classic way to give a thread its job (Module 12). Many libraries work like this: they call a method you override.
4. Test doubles
In tests, a fake class extends the real SMS or email sender and overrides send() to record the message instead of contacting a gateway. The code under test can't tell the difference.
Examples are simplified. Many modern codebases implement interfaces (Lesson 7.7) rather than extend classes for these plug-in points, but the dispatch mechanism is the same.
Fees, the overriding way
class Payment {
protected final double amount;
Payment(double amount) {
this.amount = amount;
}
double fee() {
return amount * 0.02;
}
}
class CardPayment extends Payment {
CardPayment(double amount) { super(amount); }
@Override
double fee() {
return amount * 0.018;
}
}
class UpiPayment extends Payment {
UpiPayment(double amount) { super(amount); }
@Override
double fee() {
return 0;
}
}
class WalletPayment extends Payment {
WalletPayment(double amount) { super(amount); }
}
public class Main {
public static void main(String[] args) {
Payment[] payments = {
new CardPayment(1000), new UpiPayment(1000), new WalletPayment(1000)
};
for (Payment p : payments) {
System.out.println(p.getClass().getSimpleName() + " fee: Rs " + p.fee());
}
}
}double fee()in PaymentThe default rule; WalletPayment inherits it unchanged.@Override double fee()Same signature and return type; javac confirms a parent method exists.Payment pThe loop only knows "a Payment". The object decides whichfee()runs.getClass().getSimpleName()Shows the object's real class at run time.
One variable, two objects, two fee() versions
Watch the frame label: the same line p.fee() runs Payment.fee first and Upi.fee the second time.
Worked examples: real problems, solved and traced in memory
Three business problems solved with overriding. Run each solution, then step through it: the frame label shows which class's version of a method is actually running, so you can watch the JVM pick the override at run time.
One loop, the right fee for every payment
Problem statement
A checkout page lists the convenience fee for each payment option on a Rs 1,000 order. Card charges 1.8%, UPI charges nothing, and any other method uses the default 2%.
order amount Rs 1000 (a field); options: Card, UPI, Wallet
18.0 0.0 20.0
Approach
- Put the default rule in
Payment.fee(). CardandUpioverridefee();Walletdoesn't.- Loop over a
Payment[]and callfee(); watch which version's frame appears.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
A withdraw override that adds a minimum-balance rule
Problem statement
Every account allows a withdrawal if the balance covers it. A savings account must also keep Rs 1,000 in the account. Override withdraw in Savings so it checks its extra rule and then reuses the parent's logic.
Savings balance 5000; withdraw 4500, then 3000
false true 2000.0
Approach
Account.withdrawchecks the balance and debits.Savings.withdrawfirst checks the minimum-balance rule.- If that passes, it calls
super.withdraw(amount)instead of repeating the debit code.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Static label vs overridden rate
Problem statement
Domestic invoices charge 18% GST; export invoices are zero-rated. Each class also has a static label(). Through a variable of type Invoice holding an export invoice, which label and which rate do you get?
Invoice inv = new ExportInvoice(); amount Rs 50000
GST invoice, tax 0.0
Approach
rate()is an instance method: overridden, chosen by the object.label()is static: hidden, chosen by the variable's type at compile time.- Trace both calls and compare their frames: the static call has no
this.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Check yourself
Quick revision
Lesson 7.4 in one screen
- Overriding: same signature as an inherited instance method; the child's version replaces it.
- Return: same or a subtype (covariant). Access: same or wider. Checked exceptions: same, fewer or narrower.
- Always write
@Override: javac then catches typos and wrong parameter types. finalmethods can't be overridden;privateones aren't inherited.staticmethods are hidden, chosen by the variable's type at compile time.- Instance methods are dispatched at run time by the object's class (dynamic dispatch).
- Use
super.m()inside the override to reuse the parent's version.
Interview and viva questions
What is the difference between overloading and overriding?
Overloading: same method name, different parameter lists, usually in the same class; javac picks the version at compile time from the argument types. Overriding: same signature in a subclass; the JVM picks the version at run time from the object's class. Overloading is compile-time polymorphism, overriding is runtime polymorphism (Lesson 7.5).
Can we override a static method?
No. A static method with the same signature in the child hides the parent's. Which one runs is decided at compile time by the reference type, not the object, so there's no runtime polymorphism. Also, a static method can't hide an instance method or vice versa: javac reports an error.
Why can't an overriding method have weaker access or throw broader checked exceptions?
Because a child object must be usable wherever the parent is expected. Code calling payment.fee() through a parent reference relies on the parent's promises: that the method is accessible and throws only the declared checked exceptions. Narrower access or new exceptions would break those callers.
Can we override the main method or a constructor?
Constructors aren't inherited, so they can't be overridden. main is static, so a main in a child class hides the parent's rather than overriding it; the JVM runs whichever class you launch.
Is @Override mandatory?
No, overriding works without it. But it's strongly recommended: it documents intent and makes javac verify that a matching parent method exists, catching mistakes like equals(Account) instead of equals(Object) or a misspelt name.
Practice problems
In each problem, decide which method the parent provides as a default and which children must override it. Put @Override on every override. The last three are business problems.
Animal sounds
Animal has sound() returning "...". Cow and Cat override it. Loop over an Animal[] with one of each (including a plain Animal) and print the sounds.
Animal, Cow, Cat
Animal: ... Cow: Moo Cat: Meow
Show solution and explanation
class Animal {
String sound() {
return "...";
}
}
class Cow extends Animal {
@Override
String sound() {
return "Moo";
}
}
class Cat extends Animal {
@Override
String sound() {
return "Meow";
}
}
public class Main {
public static void main(String[] args) {
Animal[] animals = { new Animal(), new Cow(), new Cat() };
for (Animal a : animals) {
System.out.println(a.getClass().getSimpleName() + ": " + a.sound());
}
}
}How it works: every element is used through an Animal variable, yet each object runs its own sound(). The plain Animal falls back to the parent's default.
Override toString for readable output
Printing a Book object shows something like Book@1b6d3586. Override toString() so System.out.println(book) prints the title and price.
"Wings of Fire", Rs 299
Wings of Fire (Rs 299.0) In cart: Wings of Fire (Rs 299.0)
Show solution and explanation
class Book {
private final String title;
private final double price;
Book(String title, double price) {
this.title = title;
this.price = price;
}
@Override
public String toString() {
return title + " (Rs " + price + ")";
}
}
public class Main {
public static void main(String[] args) {
Book b = new Book("Wings of Fire", 299);
System.out.println(b);
System.out.println("In cart: " + b);
}
}How it works: println and string concatenation call toString(), inherited from Object. Our override replaces it. It must be public, because Object's version is public and access can't be narrowed.
Extend, don't replace
Report.header() returns the company name. SalesReport overrides it to return the parent's header followed by " - Sales". Use super.
company "Shree Traders"
Shree Traders - Sales Shree Traders
Show solution and explanation
class Report {
String header() {
return "Shree Traders";
}
}
class SalesReport extends Report {
@Override
String header() {
return super.header() + " - Sales";
}
}
public class Main {
public static void main(String[] args) {
Report r = new SalesReport();
System.out.println(r.header());
System.out.println(new Report().header());
}
}How it works: the override reuses the parent's text with super.header(). Through the Report variable r, the SalesReport version still runs, because the object decides.
Covariant return
Shape.copy() returns a Shape. In Circle, override it to return a Circle, so callers can use radius without a cast.
Circle radius 7
original 7.0, copy 10.0
Show solution and explanation
class Shape {
Shape copy() {
return new Shape();
}
}
class Circle extends Shape {
double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
Circle copy() {
return new Circle(radius);
}
}
public class Main {
public static void main(String[] args) {
Circle c = new Circle(7);
Circle c2 = c.copy();
c2.radius = 10;
System.out.println("original " + c.radius + ", copy " + c2.radius);
}
}How it works: the override returns Circle, a subtype of Shape: a covariant return, allowed since Java 5. Code holding a Circle gets a Circle back, with no cast. Changing the copy doesn't affect the original because copy() creates a new object.
Static hiding vs overriding
Both Vehicle and Bus have a static category() and an instance maxSpeed(). Call both through a Vehicle variable that holds a Bus, and through a Bus variable. Predict first.
Vehicle v = new Bus(); Bus b = new Bus();
v: Vehicle, 80 b: Bus, 80
Show solution and explanation
class Vehicle {
static String category() { return "Vehicle"; }
int maxSpeed() { return 100; }
}
class Bus extends Vehicle {
static String category() { return "Bus"; }
@Override
int maxSpeed() { return 80; }
}
public class Main {
public static void main(String[] args) {
Vehicle v = new Bus();
Bus b = new Bus();
System.out.println("v: " + v.category() + ", " + v.maxSpeed());
System.out.println("b: " + b.category() + ", " + b.maxSpeed());
}
}How it works: maxSpeed() is an instance method, so the object (a Bus) decides: 80 both times. category() is static, so javac decides from the variable's type: "Vehicle" through v, "Bus" through b. Prefer Vehicle.category() to make static calls obvious.
Widen access while overriding
Sensor.read() is protected. A TemperatureSensor overrides it as public so that any class can call it. Show it works, and note which direction is allowed.
temperature reading 31.5
Temperature: 31.5 C Via Sensor: 31.5
Show solution and explanation
class Sensor {
protected double read() {
return 0;
}
}
class TemperatureSensor extends Sensor {
@Override
public double read() {
return 31.5;
}
}
public class Main {
public static void main(String[] args) {
TemperatureSensor t = new TemperatureSensor();
System.out.println("Temperature: " + t.read() + " C");
Sensor s = t;
System.out.println("Via Sensor: " + s.read());
}
}How it works: protected → public makes the method more accessible, which is allowed. The opposite (public → protected) would be "attempting to assign weaker access privileges", because callers using the parent type might lose access.
Fix the equals overload
Two Customer objects with the same mobile number should be equal, including inside a HashSet. Write a correct equals(Object) and hashCode() with @Override, and show the set keeps only one.
Customer("98200 12345"), Customer("98200 12345")Customers in set: 1
Show solution and explanation
import java.util.HashSet;
import java.util.Set;
class Customer {
private final String mobile;
Customer(String mobile) {
this.mobile = mobile;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Customer)) return false;
Customer other = (Customer) o;
return mobile.equals(other.mobile);
}
@Override
public int hashCode() {
return mobile.hashCode();
}
}
public class Main {
public static void main(String[] args) {
Set<Customer> set = new HashSet<>();
set.add(new Customer("98200 12345"));
set.add(new Customer("98200 12345"));
System.out.println("Customers in set: " + set.size());
}
}How it works: the parameter type must be Object to override Object's equals; @Override guarantees we got it right. HashSet first uses hashCode() to find the bucket, then equals to compare, so both must be overridden together (Lesson 10.3, 11.6).
Shapes for a design tool
A poster-design tool needs the total ink area of all shapes on a canvas. Shape.area() returns 0; Rect, Circle and Triangle override it. Print each area (2 decimals) and the total.
Rect 40×25, Circle r=10, Triangle base 30 height 12
Rect 1000.00 sq cm Circle 314.16 sq cm Triangle 180.00 sq cm Total 1494.16 sq cm
Show solution and explanation
class Shape {
double area() {
return 0;
}
String name() {
return getClass().getSimpleName();
}
}
class Rect extends Shape {
private final double w, h;
Rect(double w, double h) { this.w = w; this.h = h; }
@Override
double area() { return w * h; }
}
class Circle extends Shape {
private final double r;
Circle(double r) { this.r = r; }
@Override
double area() { return Math.PI * r * r; }
}
class Triangle extends Shape {
private final double base, height;
Triangle(double base, double height) { this.base = base; this.height = height; }
@Override
double area() { return 0.5 * base * height; }
}
public class Main {
public static void main(String[] args) {
Shape[] canvas = { new Rect(40, 25), new Circle(10), new Triangle(30, 12) };
double total = 0;
for (Shape s : canvas) {
System.out.printf("%-8s %8.2f sq cm%n", s.name(), s.area());
total += s.area();
}
System.out.printf("Total %8.2f sq cm%n", total);
}
}How it works:
- The canvas loop knows only
Shape; each object supplies its own formula through overriding. name()is inherited unchanged;getClass()returns the object's real class, so it prints Rect, Circle, Triangle.- Adding a Hexagon means one new class; the total-area code stays the same. (In Lesson 7.6 Shape becomes abstract, since a plain Shape has no real area.)
Diwali bonus by employee type
Everyone gets a Diwali bonus of 8.33% of basic (one month's pay per year, roughly). Managers get the normal bonus plus Rs 10,000; interns get a flat Rs 3,000. Override bonus() and print a bonus sheet.
Employee "Anil" 30000, Manager "Kiran" 70000, Intern "Zoya" 15000
Anil (Employee): Rs 2499.0 Kiran (Manager): Rs 15831.0 Zoya (Intern): Rs 3000.0 Total bonus payout: Rs 21330.0
Show solution and explanation
class Employee {
protected final String name;
protected final double basic;
Employee(String name, double basic) {
this.name = name;
this.basic = basic;
}
double bonus() {
return Math.round(basic * 0.0833);
}
}
class Manager extends Employee {
Manager(String name, double basic) { super(name, basic); }
@Override
double bonus() {
return super.bonus() + 10000;
}
}
class Intern extends Employee {
Intern(String name, double basic) { super(name, basic); }
@Override
double bonus() {
return 3000;
}
}
public class Main {
public static void main(String[] args) {
Employee[] staff = {
new Employee("Anil", 30000), new Manager("Kiran", 70000), new Intern("Zoya", 15000)
};
double total = 0;
for (Employee e : staff) {
System.out.println(e.name + " (" + e.getClass().getSimpleName() + "): Rs " + e.bonus());
total += e.bonus();
}
System.out.println("Total bonus payout: Rs " + total);
}
}How it works:
- Anil: 30000 × 0.0833 = 2499. Kiran: 70000 × 0.0833 = 5831 (rounded), + 10,000 = 15,831. Zoya: flat 3,000.
- Manager extends the rule with
super.bonus(); Intern replaces it. - The payroll loop has no if-else on job type.
Delivery time estimate by vehicle
A delivery app estimates minutes for a trip: DeliveryVehicle.eta(km) assumes 30 km/h plus 10 minutes of handling. A Bike is faster in traffic (40 km/h); a Drone flies at 60 km/h with only 2 minutes of handling but can't go beyond 10 km (return -1). Print ETAs for 6 km and 15 km.
distances 6 km and 15 km
6.0 km: DeliveryVehicle=22 min Bike=19 min Drone=8 min 15.0 km: DeliveryVehicle=40 min Bike=33 min Drone=n/a
Show solution and explanation
class DeliveryVehicle {
int eta(double km) {
return (int) Math.ceil(km / 30 * 60) + 10;
}
}
class Bike extends DeliveryVehicle {
@Override
int eta(double km) {
return (int) Math.ceil(km / 40 * 60) + 10;
}
}
class Drone extends DeliveryVehicle {
@Override
int eta(double km) {
if (km > 10) return -1;
return (int) Math.ceil(km / 60 * 60) + 2;
}
}
public class Main {
public static void main(String[] args) {
DeliveryVehicle[] fleet = { new DeliveryVehicle(), new Bike(), new Drone() };
double[] trips = {6, 15};
for (double km : trips) {
System.out.print(km + " km:");
for (DeliveryVehicle v : fleet) {
int m = v.eta(km);
System.out.print(" " + v.getClass().getSimpleName() + "=" + (m < 0 ? "n/a" : m + " min"));
}
System.out.println();
}
}
}How it works:
- Default vehicle: 6 km at 30 km/h = 12 min + 10 = 22. Bike: 9 + 10 = 19. Drone: 6 + 2 = 8.
- For 15 km the drone's override returns −1, which the caller shows as n/a.
- The dispatcher code that picks the fastest vehicle would only call
eta(); each vehicle class owns its own speed rules.
Real-world scenario questions (asked in company interviews)
Overriding bugs often compile and run, then quietly do the wrong thing. Decide your answer first, then open the model answer.
Duplicate products in a HashSet
A Product class has public boolean equals(Product p) comparing SKUs, and also a correct hashCode(). A HashSet<Product> still keeps two products with the same SKU. The equals method passes its unit test. Explain.
The interviewer is checking: do you know overloading vs overriding and why @Override matters?
Show a strong answer
equals(Product)overloadsObject.equals(Object). HashSet callsequals(Object), which is still Object's identity comparison.- The unit test called
a.equals(b)with both declared as Product, so javac picked the overload; the test proved nothing about collections. - Fix:
@Override public boolean equals(Object o), check the type, cast, compare SKUs. With@Override, the original mistake wouldn't have compiled.
Avoid: adding more tests with Product-typed variables only. Test through the collection.
Adding a payment method without touching checkout
Checkout currently has a switch on payment type to compute fees and refund rules. The business wants to add "Buy Now Pay Later". How would you restructure the code so adding the next method is easy?
The interviewer is checking: can you replace type checks with overriding?
Show a strong answer
- Create a parent
Paymentwithfee()andrefundDays()(default rules), and one subclass per method that overrides what differs. - Checkout calls
payment.fee()without knowing the type; dynamic dispatch picks the right version. - BNPL becomes one new class with its own overrides; existing classes and checkout don't change (the open/closed principle).
Avoid: adding one more case to every switch. That's how Riya's net banking bug happened.
The override that stopped being called
Your PremiumAccount overrides calculateInterest(double rate) from a vendor library's Account. After upgrading the library, premium customers get normal interest. The vendor changed the parent method to calculateInterest(BigDecimal rate). Your code had no @Override. What happened, and what would have caught it?
The interviewer is checking: do you understand how signature changes silently break overriding?
Show a strong answer
- Your method's signature no longer matches the parent's, so it became an unrelated method. The library calls its own
calculateInterest(BigDecimal), which you don't override. - With
@Override, the upgrade would have failed to compile with "method does not override or implement a method from a supertype", pointing straight at the problem. - Fix: update the override's signature, add
@Overrideeverywhere, and add a test that checks premium interest through the library's entry point.
Avoid: treating @Override as optional decoration. It's a safety check.
"I overrode the static formatter but nothing changed"
A shared DateUtil class has static String format(LocalDate d). A team writes IndiaDateUtil extends DateUtil with its own static format (dd-MM-yyyy) and passes an IndiaDateUtil object wherever a DateUtil is expected. Reports still show the old format. Why, and what design would work?
The interviewer is checking: do you know that static methods don't take part in dynamic dispatch?
Show a strong answer
- Static methods are hidden, not overridden. Calls compiled against
DateUtilalways runDateUtil.format; the object passed in is ignored. - For swappable behaviour, use instance methods: an interface
DateFormatterwith implementations, injected where needed (Lesson 7.7). - Static utility methods are fine for fixed behaviour; once you need variations, make it an object.
Avoid: calling static methods through instance variables. It compiles but suggests polymorphism that doesn't exist.