Three send methods for one message
The fest team wants reminders sent by SMS, email and WhatsApp. Riya's code has sendSms(Sms s), sendEmail(Email e), sendWhatsApp(WhatsApp w), three lists, and three loops. When Instagram DMs are added, everything has to be copied a fourth time.
Sir, every channel needs its own list, its own loop, its own send method. It's the same work three times.
Make them all children of Notification, each overriding send(). Then one list of type Notification holds all of them, and one loop calls n.send().
But how does one line know whether to send an SMS or an email?
It doesn't need to. At run time the JVM looks at the object and runs its send(). One call, many behaviours: that's polymorphism, the third pillar.
What is polymorphism?
Polymorphism ("many forms") is the ability of a single method name or reference type to work with different forms. Java has two kinds. Compile-time (static) polymorphism is achieved by method overloading: the compiler selects the method from the number and declared types of the arguments. Runtime (dynamic) polymorphism is achieved by method overriding with a superclass reference: the JVM selects the method from the actual class of the object at run time, a process called dynamic method dispatch.
"Send this message" means different things for SMS and email, but you give the same instruction. Overloading is one name for different inputs; overriding is one call for different objects.
Words you'll meet in this lesson
Notification n = new Sms();. Automatic and always safe.(Sms) n. Needs a check.n instanceof Sms.Who decides what
| Which methods can I call? | The variable's declared type, checked by javac. n.maxChars() fails if Notification doesn't have it. |
|---|---|
| Which overload? | javac, from the declared types of the arguments |
| Which override runs? | The JVM, from the object's actual class |
| Fields | Not polymorphic: chosen by the variable's type |
| Upcast | Implicit, never fails |
| Downcast | Explicit (Type); javac rejects impossible casts, the JVM throws ClassCastException for wrong objects |
Exam tip: remember the sentence "reference type decides what you can call; object type decides what actually runs". It answers most output-prediction questions on polymorphism. Overloading = compile time; overriding = run time.
Two kinds of polymorphism
Compile-time
Overloading: pay(int), pay(int, String). javac picks from the arguments.
Runtime
Overriding + a parent reference: n.send() runs the object's version.
Casting
Up (child → parent) is free; down (parent → child) needs instanceof first.
Both kinds in one program
class Notification {
String send() { return "generic"; }
}
class Sms extends Notification {
@Override
String send() { return "SMS sent"; }
}
class Email extends Notification {
@Override
String send() { return "Email sent"; }
}
public class Main {
// compile-time: overloads chosen by javac
static String remind(String to) { return "Reminder to " + to; }
static String remind(String to, int times) { return "Reminder to " + to + " x" + times; }
public static void main(String[] args) {
System.out.println(remind("Riya"));
System.out.println(remind("Riya", 3));
// runtime: overrides chosen by the JVM
Notification[] queue = { new Sms(), new Email(), new Sms() };
for (Notification n : queue) {
System.out.println(n.send());
}
}
}| Overloading (compile-time) | Overriding (runtime) | |
|---|---|---|
| Where | Usually the same class | Parent and child classes |
| Signature | Same name, different parameters | Same name, same parameters |
| Decided by | javac, from argument types | JVM, from the object's class |
| Return type | Can be anything | Same or covariant |
| static / private / final methods | Can be overloaded | Can't be overridden |
| Bytecode call | Fixed target chosen by javac | invokevirtual, resolved on the object |
Overloads look at the variable, not the object: with show(Object) and show(String), the call Object o = "hi"; show(o); runs show(Object), because javac chooses overloads from the declared type. Only overriding looks at the object at run time.
From upcast to safe downcast
Upcast: store a child in a parent variable
No conversion happens: the reference still points at the same Sms object. Only javac's view changes: throughNotification n = new Sms();nyou may call only what Notification declares.Call an overridden method
javac checks that Notification hasn.send(); // "SMS sent"send()and emitsinvokevirtual Notification.send. At run time the JVM reads the object's class (Sms) and runs the most specificsend(), using a per-class method table so this is fast.Need a child-only method? Check first
if (n instanceof Sms) { Sms s = (Sms) n; System.out.println(s.maxChars()); }instanceofasks the JVM about the object's real class. It'sfalsefornull, so it also protects against NullPointerException.Java 16+: pattern matching does both at once
If the test is true,if (n instanceof Sms s) { System.out.println(s.maxChars()); }sis declared and already cast. It exists only where the test is known to be true. Standard since Java 16 (preview in 14 and 15).A wrong cast compiles, then fails
javac allows it because a Notification could be an Sms. Every cast is checked by the JVM (aNotification n = new Email(); Sms s = (Sms) n; // ClassCastException at run timecheckcastinstruction). Impossible casts, like Notification to String, are rejected at compile time:incompatible types.
Reference type vs object type: both views
Left: the code and its result. Right: what javac checks against the variable's type, and what the JVM finds in the object.
Notification variable 6 steps- Upcast: an Sms is a Notification, so it can be stored in a Notification variable with no cast.
- Dynamic dispatch:
n.send()printsSMS sent, the Sms version, althoughnis declared as Notification. - Many forms: the same variable now holds an Email, and the same call prints
Email sent. - Reference type limits calls:
maxChars()exists only in Sms. javac sees a Notification variable and refuses. - instanceof:
nholds an Email, so the test is false and the else branch runs. Nothing is cast. - Bad downcast: forcing
(Sms) ncompiles, but the JVM finds an Email and throwsClassCastException.
- The variable has a declared type (Notification) that javac uses; the object has a real class (Sms) that the JVM uses.
- javac checks
send()exists in Notification. At run time the JVM looks upsendin the object's class, Sms, and runs it. - After
n = new Email(), the sameinvokevirtualinstruction finds Email'ssend(). - Method availability is decided at compile time from the declared type. The object's extra methods are invisible through
n. instanceofis evaluated at run time on the object. Email is not an Sms, so the guarded code is skipped.- A cast is a promise to javac, checked again by the JVM's
checkcast. A broken promise throwsClassCastException.
Polymorphism in real systems
Polymorphism is what lets large systems add new types without editing old code.
Watch it with an example: assigning a delivery to the right vehicle
A dispatch screen keeps one Vehicle v variable. Watch what javac allows through that variable, what the JVM runs, and when a downcast is safe.
- Upcast: the dispatcher stores a Bike in a
Vehiclevariable. Every Bike IS-A Vehicle, so no cast is needed. - Runtime polymorphism:
v.eta(6)runs Bike's override: 6 km at 40 km/h = 9 min, plus 10 min handling = 19 min. - Many forms: the same variable now holds a Drone; the same call runs Drone's version: 6 + 2 = 8 min.
- Compile-time limit:
battery()exists only on Drone. javac checks the variable's type, Vehicle, and refuses. - Safe downcast:
instanceof Drone dchecks the real object and gives a Drone-typed variable, sod.battery()works. - Unsafe downcast: forcing
(Bike) vcompiles, but the object is a Drone, so the JVM throwsClassCastException.
1. Notification and messaging services
An alert service keeps a list of channels and calls channel.send(message) on each. SMS, email, push and chat channels are separate classes; adding one doesn't touch the alert logic.
2. Coding to the collection interfaces
Java code declares List<String> names = new ArrayList<>();. Changing to a LinkedList later changes one line, because everything else talks to the List type (Module 11).
3. Design and drawing tools
A canvas holds a list of shapes and calls shape.draw(g) and shape.area() on each. Each shape class knows how to draw itself.
4. Exception handling
A catch (IOException e) block catches every subclass, and e.getMessage() runs the actual exception's version. Exception handling relies on upcasting (Module 9).
Examples are simplified. Real systems usually define these plug-in points as interfaces (Lesson 7.7); the dispatch works the same way.
One queue, every channel
class Notification {
protected final String to;
Notification(String to) {
this.to = to;
}
String send() {
return "Notify " + to;
}
}
class Sms extends Notification {
Sms(String to) { super(to); }
@Override
String send() { return "SMS to " + to; }
int maxChars() { return 160; }
}
class Email extends Notification {
Email(String to) { super(to); }
@Override
String send() { return "Email to " + to; }
}
public class Main {
public static void main(String[] args) {
Notification[] queue = {
new Sms("98200 11111"), new Email("riya@college.in"), new Notification("Arjun")
};
for (Notification n : queue) {
System.out.println(n.send());
if (n instanceof Sms s) {
System.out.println(" (limit " + s.maxChars() + " chars)");
}
}
}
}Notification[] queue = { new Sms(..), ... }Upcasting: each child object is stored as a Notification.n.send()Dynamic dispatch: each object's ownsend()runs.n instanceof Sms sJava 16 pattern matching: test and cast in one step;sexists only inside the if.s.maxChars()Allowed becausesis declared as Sms.
Upcast, dispatch, check, downcast
Notice that n's declared type never changes, while the frame label shows the object's class.
Worked examples: real problems, solved and traced in memory
Three business problems where one parent-type variable meets objects of different classes. Run each solution, then step through it: compare the variable's type on the stack with the object's class on the heap, and watch which method's frame is pushed.
One loop sends every alert
Problem statement
When an order ships, the customer gets an SMS and an email. The alert loop must not care which is which, except that SMS messages must report their character limit.
queue: Sms("98200 11111"), Email("meera@mail.in")SMS to 98200 11111 limit 160 Email to meera@mail.in
Approach
- Both classes extend
Notificationand overridesend(). - Store them in a
Notification[](upcasting) and callsend()in one loop. - Use
instanceof Sms sfor the SMS-onlylimit().
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.
Overload picked by javac, override picked by the JVM
Problem statement
A design tool's Canvas has two overloaded render methods: a general one for any Shape and a special one for Circle. A circle stored in a Shape variable is rendered. Which render runs, and which area()?
Shape s = new Circle(); radius 2
generic shape, area 12.56
Approach
render(Shape)andrender(Circle)are overloads: javac picks one from the declared type.area()is overridden: the JVM picks from the object.- Trace the call and read both frame labels.
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.
Set temperature only on refrigerated trucks
Problem statement
Before a vaccine shipment, every refrigerated vehicle in the fleet must be set to −18 °C. The fleet list also has ordinary trucks. Set the temperature without risking a ClassCastException, and count how many were set.
fleet: Reefer, Truck, Reefer
2 reefers set to -18
Approach
- The fleet is a
Truck[];Reefer extends TruckaddssetTemp(). - Use
instanceof Reefer rto test and downcast in one step. - Only matching objects get the call; others are skipped safely.
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.5 in one screen
- Polymorphism = one name, many forms.
- Compile-time: overloading, chosen by javac from declared argument types.
- Runtime: overriding through a parent reference, chosen by the JVM from the object's class.
- Reference type decides what you can call; object type decides what runs. Fields follow the reference type.
- Upcasting is automatic; downcasting needs
(Type)and can throwClassCastException. - Guard downcasts with
instanceof; it's false for null. - Java 16+:
if (x instanceof Sms s)tests and casts in one step.
Interview and viva questions
What is dynamic method dispatch?
It's the mechanism by which a call to an overridden method through a superclass reference is resolved at run time, based on the actual object's class. javac emits an invokevirtual instruction naming the method; the JVM looks it up starting from the object's class (using per-class method tables) and runs the most specific version.
Why are static, private and final methods statically bound?
Static methods belong to the class, private methods aren't inherited, and final methods can't be overridden. In each case there's only one possible target, so the call can be fixed at compile time. Only overridable instance methods need run-time dispatch.
What is the difference between upcasting and downcasting?
Upcasting converts a child reference to a parent type (Animal a = dog;); it's implicit and always safe. Downcasting converts a parent reference to a child type (Dog d = (Dog) a;); it's explicit, needed to access child-only members, and fails with ClassCastException if the object isn't really that child type.
Is excessive instanceof a design smell?
Often, yes. A chain of if (x instanceof A) … else if (x instanceof B) usually means behaviour that should be an overridden method. Use instanceof for genuine special cases (one channel with an extra limit), not as a replacement for polymorphism. (Modern Java's sealed classes and pattern-matching switch make exhaustive type checks safer, but that's beyond this lesson.)
Are fields polymorphic in Java?
No. Field access is resolved at compile time from the reference's declared type. If parent and child both declare limit, parentRef.limit reads the parent's field even when the object is a child. That's another reason to keep fields private and access them through overridable methods.
Practice problems
For every call in these problems, ask two questions: what is the variable's declared type (javac's view) and what is the object's real class (the JVM's view)? The last three are business problems.
Upcast and call
Store a Circle and a Square in variables of type Shape and call the overridden draw() on each.
Shape s1 = new Circle(); Shape s2 = new Square();
Drawing a circle Drawing a square
Show solution and explanation
class Shape {
void draw() { System.out.println("Drawing a shape"); }
}
class Circle extends Shape {
@Override
void draw() { System.out.println("Drawing a circle"); }
}
class Square extends Shape {
@Override
void draw() { System.out.println("Drawing a square"); }
}
public class Main {
public static void main(String[] args) {
Shape s1 = new Circle();
Shape s2 = new Square();
s1.draw();
s2.draw();
}
}How it works: both variables are declared as Shape, so javac allows draw(). At run time the JVM looks at each object and runs Circle's and Square's versions: runtime polymorphism.
Overloaded area methods
Write three overloaded static methods named area: one for a square (side), one for a rectangle (length, width) and one for a circle (radius as a double). Call all three.
area(5), area(4, 6), area(3.0)
Square: 25 Rectangle: 24 Circle: 28.27
Show solution and explanation
public class Main {
static int area(int side) {
return side * side;
}
static int area(int length, int width) {
return length * width;
}
static double area(double radius) {
return Math.PI * radius * radius;
}
public static void main(String[] args) {
System.out.println("Square: " + area(5));
System.out.println("Rectangle: " + area(4, 6));
System.out.printf("Circle: %.2f%n", area(3.0));
}
}How it works: same name, different parameter lists. javac picks the version from the number and types of arguments at compile time: compile-time polymorphism. area(3.0) matches the double version; area(3) would match the int version.
instanceof up and down
For an Animal variable holding a Dog, print whether it is an instance of Animal, Dog and Cat. Then do the same for a plain Animal.
Animal a = new Dog(); Animal b = new Animal();
a: Animal true, Dog true, Cat false b: Animal true, Dog false, Cat false
Show solution and explanation
class Animal { }
class Dog extends Animal { }
class Cat extends Animal { }
public class Main {
public static void main(String[] args) {
Animal a = new Dog();
Animal b = new Animal();
System.out.println("a: Animal " + (a instanceof Animal) + ", Dog " + (a instanceof Dog) + ", Cat " + (a instanceof Cat));
System.out.println("b: Animal " + (b instanceof Animal) + ", Dog " + (b instanceof Dog) + ", Cat " + (b instanceof Cat));
}
}How it works: instanceof checks the object, not the variable. a is declared as Animal but holds a Dog, so a instanceof Dog is true. A plain Animal is not a Dog.
Pattern matching for instanceof
A list of Object values contains a String, an Integer and a Double. Use Java 16 pattern matching to print the String's length, the Integer doubled and the Double rounded.
Object[] values = {"Pune", 21, 3.7}String of length 4 Integer doubled: 42 Double rounded: 4
Show solution and explanation
public class Main {
public static void main(String[] args) {
Object[] values = {"Pune", 21, 3.7};
for (Object v : values) {
if (v instanceof String s) {
System.out.println("String of length " + s.length());
} else if (v instanceof Integer i) {
System.out.println("Integer doubled: " + (i * 2));
} else if (v instanceof Double d) {
System.out.println("Double rounded: " + Math.round(d));
}
}
}
}How it works: each test both checks the type and declares a correctly typed variable (s, i, d), so no separate cast line is needed. The literals 21 and 3.7 were autoboxed to Integer and Double (Lesson 10.2).
Avoid a ClassCastException
A fleet has trucks and bikes stored as Vehicle. Print the total capacity of the trucks only, safely, without any cast that could fail.
Truck 9000 kg, Bike, Truck 6000 kg
Truck capacity: 15000 kg (skipped 1)
Show solution and explanation
class Vehicle { }
class Truck extends Vehicle {
final int capacityKg;
Truck(int capacityKg) { this.capacityKg = capacityKg; }
}
class Bike extends Vehicle { }
public class Main {
public static void main(String[] args) {
Vehicle[] fleet = { new Truck(9000), new Bike(), new Truck(6000) };
int total = 0;
int skipped = 0;
for (Vehicle v : fleet) {
if (v instanceof Truck t) {
total += t.capacityKg;
} else {
skipped++;
}
}
System.out.println("Truck capacity: " + total + " kg (skipped " + skipped + ")");
}
}How it works: a direct (Truck) v would throw ClassCastException on the Bike. The instanceof guard only casts when the object really is a Truck. In a bigger design, a capacityKg() method on Vehicle (returning 0 for bikes) would remove the need for the check.
Overload chosen by declared type
A Printer class has print(Object) and print(String). Call it with a String variable, an Object variable holding a String, and a String cast back from that Object. Predict which version runs each time.
String s = "invoice"; Object o = s;
print(String): invoice print(Object): invoice print(String): invoice
Show solution and explanation
public class Main {
static void print(Object o) {
System.out.println("print(Object): " + o);
}
static void print(String s) {
System.out.println("print(String): " + s);
}
public static void main(String[] args) {
String s = "invoice";
Object o = s;
print(s);
print(o);
print((String) o);
}
}How it works: all three calls pass the same object, but javac chooses overloads from the declared types: String, Object, String. The object's real class is never consulted for overloads. That's the key difference from overriding.
Fields vs methods through a parent reference
Both Plan and GoldPlan declare a field discount and a method getDiscount(). Through a Plan variable holding a GoldPlan, print both. Explain the difference.
Plan p = new GoldPlan();
p.discount = 5 p.getDiscount() = 15
Show solution and explanation
class Plan {
int discount = 5;
int getDiscount() { return discount; }
}
class GoldPlan extends Plan {
int discount = 15;
@Override
int getDiscount() { return discount; }
}
public class Main {
public static void main(String[] args) {
Plan p = new GoldPlan();
System.out.println("p.discount = " + p.discount);
System.out.println("p.getDiscount() = " + p.getDiscount());
}
}How it works: fields are resolved at compile time from the variable's type, so p.discount reads Plan's field (5). Methods are dispatched on the object, so getDiscount() runs GoldPlan's version, which reads GoldPlan's field (15). Keep fields private and use methods to avoid this confusion.
Payment processing with special cases
A checkout processes a list of payments polymorphically with process(). Card payments additionally need an OTP step; only for those, call sendOtp() using pattern matching. Print the log for a card, a UPI and a cash-on-delivery payment.
Card Rs 2499, UPI Rs 349, COD Rs 799
OTP sent to registered mobile Card charged Rs 2499.0 UPI collect request for Rs 349.0 Collect Rs 799.0 at the door Collected online: Rs 2848.0
Show solution and explanation
class Payment {
protected final double amount;
Payment(double amount) { this.amount = amount; }
String process() { return "Processed Rs " + amount; }
}
class CardPayment extends Payment {
CardPayment(double amount) { super(amount); }
@Override
String process() { return "Card charged Rs " + amount; }
String sendOtp() { return "OTP sent to registered mobile"; }
}
class UpiPayment extends Payment {
UpiPayment(double amount) { super(amount); }
@Override
String process() { return "UPI collect request for Rs " + amount; }
}
class CashOnDelivery extends Payment {
CashOnDelivery(double amount) { super(amount); }
@Override
String process() { return "Collect Rs " + amount + " at the door"; }
}
public class Main {
public static void main(String[] args) {
Payment[] payments = { new CardPayment(2499), new UpiPayment(349), new CashOnDelivery(799) };
double online = 0;
for (Payment p : payments) {
if (p instanceof CardPayment c) {
System.out.println(c.sendOtp());
}
System.out.println(p.process());
if (!(p instanceof CashOnDelivery)) {
online += p.amount;
}
}
System.out.println("Collected online: Rs " + online);
}
}How it works:
- The common step,
process(), is polymorphic: each class supplies its own text. - The extra OTP step exists only for cards, so it's a card-only method reached with
instanceof CardPayment c. - 2499 + 349 = 2848 is collected online; COD is excluded with a negated instanceof check.
Payroll with a manager-only bonus
A monthly payroll loops over Employee[] and calls the overridden monthlyPay(). Managers also get Rs 2,000 per team member as a team bonus, available only on Manager. Print each line and the total.
Employee "Anil" 40000; Manager "Kiran" 80000, team 6; Employee "Sara" 45000
Anil Rs 40,000.00 Kiran Rs 100,000.00 Sara Rs 45,000.00 Total Rs 185,000.00
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 monthlyPay() { return basic; }
}
class Manager extends Employee {
private final int teamSize;
Manager(String name, double basic, int teamSize) {
super(name, basic);
this.teamSize = teamSize;
}
@Override
double monthlyPay() { return basic * 1.10; }
double teamBonus() { return teamSize * 2000; }
}
public class Main {
public static void main(String[] args) {
Employee[] staff = {
new Employee("Anil", 40000), new Manager("Kiran", 80000, 6), new Employee("Sara", 45000)
};
double total = 0;
for (Employee e : staff) {
double pay = e.monthlyPay();
if (e instanceof Manager m) {
pay += m.teamBonus();
}
System.out.printf("%-6s Rs %,.2f%n", e.name, pay);
total += pay;
}
System.out.printf("Total Rs %,.2f%n", total);
}
}How it works:
monthlyPay()is overridden: Kiran gets 80000 × 1.10 = 88,000 through dynamic dispatch.- The team bonus exists only on Manager, so we downcast safely with pattern matching: 6 × 2,000 = 12,000.
- Total: 40,000 + 1,00,000 + 45,000 = 1,85,000.
Delivery vehicles: who can take this order?
For a 25 kg order going 8 km, ask every vehicle in the fleet canTake(kg, km) (overridden per type) and print which vehicles qualify. Bikes carry up to 20 kg within 15 km; vans up to 500 kg any distance; drones up to 5 kg within 10 km.
order 25 kg, 8 km; fleet: Bike, Van, Drone, Van
Order: 25 kg, 8 km BIKE-1 no VAN-2 can take it DRONE-3 no VAN-4 can take it
Show solution and explanation
class Vehicle {
private static int counter = 0;
protected final String id;
Vehicle(String prefix) {
counter++;
id = prefix + counter;
}
boolean canTake(int kg, int km) { return false; }
}
class Bike extends Vehicle {
Bike() { super("BIKE-"); }
@Override
boolean canTake(int kg, int km) { return kg <= 20 && km <= 15; }
}
class Van extends Vehicle {
Van() { super("VAN-"); }
@Override
boolean canTake(int kg, int km) { return kg <= 500; }
}
class Drone extends Vehicle {
Drone() { super("DRONE-"); }
@Override
boolean canTake(int kg, int km) { return kg <= 5 && km <= 10; }
}
public class Main {
public static void main(String[] args) {
Vehicle[] fleet = { new Bike(), new Van(), new Drone(), new Van() };
int kg = 25, km = 8;
System.out.println("Order: " + kg + " kg, " + km + " km");
for (Vehicle v : fleet) {
System.out.println(" " + v.id + (v.canTake(kg, km) ? " can take it" : " no"));
}
}
}How it works:
- Each vehicle type owns its rule; the dispatcher only calls
canTake. - The static counter in the parent gives each vehicle a unique id across all types.
- 25 kg rules out the bike (20 kg) and the drone (5 kg); both vans qualify.
Real-world scenario questions (asked in company interviews)
Polymorphism removes if-else chains; misused casts bring crashes. Decide your answer first, then open the model answer.
Adding WhatsApp alerts
An order service sends alerts with if (type == SMS) sendSms(); else if (type == EMAIL) sendEmail(); in four different places. The product team wants WhatsApp added next sprint. What would you propose?
The interviewer is checking: can you replace type checks with runtime polymorphism?
Show a strong answer
- Create a parent type
Notification(or interfaceChannel) withsend(message); SMS, email and WhatsApp classes override it. - The order service keeps a list of channels and calls
channel.send(msg); dynamic dispatch picks the right implementation. - WhatsApp becomes a new class plus one line of configuration. The four if-else blocks disappear.
Avoid: adding a fifth else if to each of the four places.
ClassCastException after a new vehicle type
A dispatch job loops over List<Vehicle> and does Truck t = (Truck) v; because "all our vehicles are trucks". Operations added electric bikes yesterday, and the nightly job crashed with ClassCastException. How do you fix it properly?
The interviewer is checking: do you understand unsafe downcasts and the polymorphic alternative?
Show a strong answer
- Immediate fix: guard with
if (v instanceof Truck t)and handle other vehicles explicitly (skip with a log line, or a different path). - Better: move the behaviour the job needs (
capacityKg(),canCarry(parcel)) intoVehicleas an overridable method, so no cast is needed at all. - Add a test with a mixed fleet so a new vehicle type can't break the job silently again.
Avoid: catching ClassCastException and ignoring it. That hides real data problems.
The overload that never runs
A FeeCalculator has fee(Payment p) and a special fee(UpiPayment p) returning 0. Checkout calls calculator.fee(payment) where payment is declared as Payment. UPI users are being charged. Why?
The interviewer is checking: do you know overloads are chosen at compile time?
Show a strong answer
- Overload resolution uses the declared type of the argument. The variable is a
Payment, so javac always picksfee(Payment), even when the object is a UpiPayment. - Fix: move the rule onto the payment classes as an overridden method (
payment.fee()), so the JVM dispatches on the object. - Alternatively use
instanceofinside one method, but overriding scales better.
Avoid: assuming overloading behaves like overriding. Only overriding looks at the object.
A 40-branch instanceof chain
A report exporter has one method with 40 instanceof branches, one per document type, each building the export differently. Every new document type means editing this method, and merge conflicts happen weekly. How would you refactor it?
The interviewer is checking: can you move type-specific behaviour to where it belongs?
Show a strong answer
- Give the document hierarchy an overridable
export(Exporter e)(or an interfaceExportable), and move each branch into its own class. - If exporting must stay outside the documents (for example, many export formats), use a registry: a map from document type to an exporter object, with polymorphic exporter classes.
- Migrate gradually: move branches one at a time behind tests, keeping the old method as a fallback until it's empty.
Avoid: a big-bang rewrite. Refactor incrementally with tests.