Inheritance & polymorphism: interview questions & scenarios
These are the inheritance and polymorphism questions I ask most, and very few of them are about definitions. A five-line snippet with a parent reference, an overloaded method and a constructor tells me in two minutes whether you can separate what the compiler decides from what the JVM decides at runtime.
Here's how to use this page: read the question, say your answer out loud first, then open the model answer. The "What if..." follow-ups are the ones I actually ask next. If one stumps you, go back to the lessons: A guide to inheritance, The super keyword, Method overloading and overriding, Object type casting and The instanceof operator.
What's the difference between method overloading and method overriding?
Tests compile-time vs runtime polymorphism, signatures
Show answerHide answer
Model answer
Overloading is several methods with the same name and different parameter lists, usually in one class. The compiler picks one using the static types of the arguments and writes that exact signature into the bytecode.
Overriding is a subclass replacing an inherited instance method with the same name and parameter types. The JVM picks the implementation at runtime from the class of the receiver object. An override needs a same or covariant return type, equal or wider access, and no new or broader checked exceptions.
How I’d say it in the interview
"Overloading is compile-time: same name, different parameters, the compiler picks the signature from the argument types. Overriding is runtime: same signature in a subclass, and the JVM picks the body from the object's class. I put @Override on every override so a typo becomes a compile error instead of a silent overload."
Signals depth
Saying that the return type isn't part of the overload signature (two methods differing only in return type don't compile), and that overload choice uses argument static types while override choice uses the receiver's runtime class, so Java dispatches on one object only.
Red flags
"Overloading is when the subclass changes the parameters." Also calling both "polymorphism" without saying who decides and when.
Follow-ups interviewers ask next
What if I write
boolean equals(Order other)in myOrderclass?That's an overload, not an override, because
Object.equalstakesObject.HashSetandList.containscallequals(Object), which is stillObject's identity check, so duplicates slip through.@Overridewould have turned it into a compile error.
What does this print?
Tests field hiding, static method hiding, dynamic dispatch
Show answerHide answer
Model answer
class Payment {
String channel = "generic";
String channel() { return channel; }
static String gateway() { return "default"; }
}
class UpiPayment extends Payment {
String channel = "upi";
@Override String channel() { return channel; }
static String gateway() { return "npci"; }
}
Payment p = new UpiPayment();
System.out.println(p.channel); // generic
System.out.println(p.channel()); // upi
System.out.println(p.gateway()); // default
Only the instance method is polymorphic. Field access and static calls are resolved by the compiler from the static type of p, which is Payment. The object actually holds two channel fields, and the override reads its own.
How I’d say it in the interview
"Generic, upi, default. Fields and static methods are hidden, not overridden, so the declared type decides. Instance methods are dispatched on the object, so the override runs and reads the subclass field."
Signals depth
Naming the bytecode: getfield Payment.channel and invokestatic Payment.gateway are bound by javac, while invokevirtual Payment.channel is resolved through the vtable at runtime.
Red flags
"upi, upi, npci", which treats fields and statics as polymorphic. Also missing that the object has two channel fields.
Follow-ups interviewers ask next
What if
pisnullwhen callingp.gateway()?It still prints
default. A static call through a reference never reads the reference, so there's noNullPointerException. That's one more reason IDEs flag static access through an instance.
Can you override a static method or a private method?
Tests hiding, private methods, invokestatic
Show answerHide answer
Model answer
No to both. A static method with the same signature in a subclass hides the parent's; the call is bound at compile time to the static type, and @Override on it is a compile error. An instance method can't override a static one or the reverse; both are compile errors.
A private method isn't inherited, so a same-signature method in the subclass is a brand-new, unrelated method. Calls inside the parent to its private method always run the parent's version, whatever the object's class.
How I’d say it in the interview
"Static methods are hidden, chosen by the reference type at compile time. Private methods aren't inherited, so a subclass method with the same name is a different method. Only accessible, non-static, non-final instance methods can be overridden."
Signals depth
Mentioning that a package-private method also can't be overridden from another package, and that final is the third way to block overriding.
Red flags
"Yes, you can override static methods, it just works differently." Hiding is not overriding.
Follow-ups interviewers ask next
What if the parent's public method calls its private helper, and the subclass declares a helper with the same name?
The parent's code still calls the parent's private helper. There's no slot to override, so the subclass method only runs when the subclass calls it directly.
Why doesn't Java allow a class to extend two classes, but allows many interfaces?
Tests diamond problem, default methods
Show answerHide answer
Model answer
Because of the diamond problem. With two parent classes that share an ancestor, there's no good answer to "how many copies of the ancestor's fields does the object have?" or "which overridden body runs?". Interfaces have no instance fields, so the state half of the diamond disappears.
Since Java 8 interfaces can carry default methods, so behaviour can still clash. The rules: a class's method always wins over a default, a more specific interface wins over a less specific one, and otherwise the compiler forces you to override and pick with Refundable.super.policy().
How I’d say it in the interview
"One superclass so every object has one unambiguous chain of state and method bodies. Interfaces add contracts without fields, and if two default methods collide, the compiler makes me override and choose with X.super.m()."
Signals depth
Stating the three resolution rules in order, and knowing that the error message is "inherits unrelated defaults".
Red flags
"Java doesn't support multiple inheritance at all." It supports multiple inheritance of type and, since Java 8, of behaviour through defaults, just not of state.
Follow-ups interviewers ask next
What if one parent is a class and the interface default has the same signature as a class method?
The class wins, silently.
Payment.policy()beatsRefundable.policy()even ifPayment's method isn't annotated. It's the rule that keeps adding a default to an interface from changing existing classes.
What does this print?
Tests overload resolution, static types, null argument
Show answerHide answer
Model answer
class Notifier {
void send(Payment p) { System.out.println("payment"); }
void send(CardPayment c) { System.out.println("card"); }
void send(Object o) { System.out.println("object"); }
}
Payment p = new CardPayment();
Notifier n = new Notifier();
n.send(p); // payment
n.send((CardPayment) p); // card
n.send(null); // card
Overload choice uses the static type of the argument. p is declared Payment, so send(Payment) wins even though the object is a CardPayment. The cast changes the static type, so the card overload is chosen. For null, all three apply and the compiler picks the most specific: CardPayment is a subtype of both others.
How I’d say it in the interview
"Payment, card, card. The compiler chooses the overload from declared types, not from the object. With null, it picks the most specific applicable parameter type, which is CardPayment."
Signals depth
Explaining that Java is single dispatch: the receiver's runtime class picks the override, but arguments never influence it. That's why the Visitor pattern exists.
Red flags
"card, card, object" from thinking overloads are picked at runtime, or "null throws".
Follow-ups interviewers ask next
What if I add
void send(String s)?Then
n.send(null)doesn't compile: "reference to send is ambiguous", becauseStringandCardPaymentare unrelated and neither is more specific. You'd fix the call with a cast,n.send((CardPayment) null).What if a subclass
LoudNotifieroverridessend(Payment)and I call it through aNotifierreference?The compiler still picks the signature
send(Payment)from the argument's static type, and then the JVM dispatches that signature on the receiver, soLoudNotifier.send(Payment)runs. Two decisions, by two different parties.
In what order do static blocks, instance initializers and constructors run across a hierarchy?
Tests class initialisation, constructor chaining, super()
Show answerHide answer
Model answer
class Payment {
static { System.out.println("Payment static"); }
{ System.out.println("Payment init"); }
Payment() { System.out.println("Payment()"); }
}
class CardPayment extends Payment {
static { System.out.println("CardPayment static"); }
{ System.out.println("CardPayment init"); }
CardPayment() { System.out.println("CardPayment()"); }
}
new CardPayment();
new CardPayment();
Payment static
CardPayment static
Payment init
Payment()
CardPayment init
CardPayment()
Payment init
Payment()
CardPayment init
CardPayment()
Static blocks run once, parent first, when each class is initialised. For every object, the constructor first calls super() (inserted by javac), then runs its own class's field initializers and instance blocks, then its body. So parent init and constructor finish before any child field is initialised.
How I’d say it in the interview
"Statics once, parent then child, at first use. Then per object: parent initializers and constructor, then child initializers and constructor. javac puts the child's initializers right after the super() call inside each constructor."
Signals depth
Knowing that instance initializers are copied into every constructor that calls super(...) (not into ones that call this(...)), and that a parent's static block doesn't run again for the second object.
Red flags
"Child static block runs first", or thinking instance initializers run before super().
Follow-ups interviewers ask next
What if
CardPaymenthas two constructors and one callsthis(...)on the other?The initializers run once per object, inside the constructor that calls
super(...). Constructors that delegate withthis(...)don't repeat them, which is why chaining doesn't double-initialise.
When does a cast fail at compile time, and when does it throw ClassCastException?
Tests checkcast, downcasting, null casts
Show answerHide answer
Model answer
javac rejects a cast it can prove can never succeed: between unrelated classes such as sibling CardPayment and UpiPayment, or from a final class to an interface it doesn't implement. Everything else compiles.
A downcast like (CardPayment) payment compiles to checkcast, which runs at runtime. null passes, an object whose class is CardPayment or a subtype passes unchanged, and anything else throws ClassCastException. Casts never change the object; they change the type the compiler checks against.
How I’d say it in the interview
"Compile error when the types can't possibly overlap, runtime checkcast otherwise. Null always passes the cast and blows up later on a method call. Upcasting is free and implicit, and it doesn't change which override runs."
Signals depth
Mentioning that casting a non-final class to an unrelated interface compiles, because a future subclass could implement it, and that casts to a generic T are unchecked and fail later at the caller.
Red flags
"Upcasting makes p.fee() call the parent's fee." Casts don't touch the object.
Follow-ups interviewers ask next
What if the value comes from a
Map<String, Object>parsed from JSON?Cast to the widest type you're sure of.
(Integer) map.get("amount")breaks as soon as a number exceedsInteger.MAX_VALUEand the parser returns aLong;((Number) map.get("amount")).longValue()doesn't. Better still, bind the JSON to a typed record.
What does null instanceof Object return, and what changed with pattern matching?
Tests instanceof semantics, flow scoping, equals
Show answerHide answer
Model answer
false. instanceof returns false for null and never throws, so x != null && x instanceof Order has a redundant null check. If the static type can never be the target, like two sibling classes, it's a compile error.
Since Java 16, if (p instanceof CardPayment card) checks, casts and binds in one step. The binding is in scope wherever the match is proven: on the right of &&, inside the if, and after if (!(p instanceof CardPayment card)) return;. It's not in scope on the right of ||.
How I’d say it in the interview
"False, and no exception. Pattern matching removes the check-then-cast duplication, and javac's flow scoping only lets me use the variable where the match is guaranteed. I like the negated form as a guard clause."
Signals depth
Knowing that javac still emits instanceof then checkcast, and that in equals an instanceof check on a non-final class can break symmetry, while getClass() breaks with framework proxies.
Red flags
"instanceof throws NPE on null." Or using getClass() == everywhere without knowing the trade-off.
Follow-ups interviewers ask next
What if
equalsusesgetClass()and the entity comes back as a Hibernate lazy proxy?The proxy is a runtime subclass, so
getClass()differs and two objects for the same row aren't equal. Sets keep duplicates. Useinstanceofand compare ids through getters, because the proxy's own fields are empty.
Why is it dangerous to call an overridable method from a constructor?
Tests construction order, dynamic dispatch, constant variables
Show answerHide answer
Model answer
class Payment {
Payment() { System.out.println(describe()); }
String describe() { return "payment"; }
}
class CardPayment extends Payment {
private final String last4;
private int retries = 3;
CardPayment(String last4) { super(); this.last4 = last4; }
@Override String describe() { return "card *" + last4 + " retries=" + retries; }
}
new CardPayment("4242"); // prints: card *null retries=0
Dispatch already uses the real class while the parent constructor runs, so describe() runs the override. But the child's field initializers and constructor body haven't run yet, so it sees default values. The timeline below shows the five steps.
Five steps of constructing a CardPayment: allocation with zeroed fields, the CardPayment constructor calling super, the Payment constructor calling describe which reads null and 0, then field initializers and the constructor body setting the real values
How I’d say it in the interview
"It prints card *null retries=0. The parent constructor runs first, but the object is already a CardPayment, so the override runs against fields that are still null and zero. Effective Java says constructors must not call overridable methods, directly or indirectly."
Signals depth
Noticing that final doesn't help (last4 is still null), except for a compile-time constant like final String brand = "VISA", which javac inlines, so it prints VISA.
Red flags
"It prints payment", or "it throws because the field is final".
Follow-ups interviewers ask next
What if the override does
last4.length()?A
NullPointerExceptionfrom inside the parent's constructor, with a stack trace that points at the subclass. Fix it by making the methodprivate,finalorstatic, or by moving the call out of the constructor into a factory method.
How does the JVM decide which overridden method to run?
Tests invokevirtual, vtables, itables, JIT inlining
Show answerHide answer
Model answer
javac compiles payment.fee() to invokevirtual Payment.fee:()J, which names the method but not the implementation. At runtime the JVM reads the receiver's class pointer, looks up fee at its fixed slot in that class's vtable, and jumps. Each subclass copies its parent's vtable and replaces the slots it overrides. Interface calls use invokeinterface and an itable.
Static calls, constructors and super.m() use instructions that name an exact method with no lookup. The diagram compares the four.
The four invoke instructions: invokestatic and invokespecial bind to a fixed method, while invokevirtual and invokeinterface read the receiver's class and go through a vtable or itable
How I’d say it in the interview
"invokevirtual with a vtable: the receiver's class pointer, then a fixed slot index, then an indirect jump. The JIT usually does better by profiling the call site and inlining the one or two classes it actually sees."
Signals depth
Explaining inline caches: monomorphic sites are inlined behind a class check, bimorphic sites inline two targets, and megamorphic sites (three or more classes) fall back to a real vtable or itable call. Also class hierarchy analysis, which devirtualises when only one implementation is loaded.
Red flags
"The JVM searches up the hierarchy for a matching method on every call." That's what resolution does once, not dispatch.
Follow-ups interviewers ask next
What if a
super.fee()call is insideCardPayment.fee()?It compiles to
invokespecial Payment.fee, which runs exactlyPayment.feewith no vtable lookup. Otherwisesuper.fee()would dispatch back to the override and recurse forever.
A teammate made a @Transactional service method final. Now data isn't rolled back on errors and some calls hit a NullPointerException on the repository. What happened?
Tests proxies by subclassing, final methods, overriding
Show answerHide answer
Model answer
Spring creates class-based (CGLIB) proxies by subclassing your bean and overriding its methods to wrap them in a transaction. A final method can't be overridden, so the proxy inherits it unchanged. Calls go straight into your code with no transaction, and nothing fails at startup.
The NPE comes from where that code runs. The proxy is a separate instance whose own fields were never injected; the real bean sits behind it. Since the final method runs on the proxy instance itself, it reads the proxy's repository field, which is null.
How I’d say it in the interview
"Spring's proxy is a subclass. Final methods can't be overridden, so the proxy can't intercept them: no transaction, and the method runs on the proxy object, whose injected fields are null. Remove final, or make the class implement an interface and use an interface proxy."
Signals depth
Mentioning that private methods and self-invocation (this.save() from another method in the same bean) bypass the proxy for the same reason, and that Kotlin needs the all-open plugin because its classes are final by default.
Red flags
"Spring doesn't support final methods" without explaining that the mechanism is overriding.
Follow-ups interviewers ask next
What if the method isn't final, but it's called from another method in the same class?
this.save()calls the real object directly, not the proxy, so the annotation is ignored. Move the method to another bean, or call through the injected proxy.
After upgrading a shared library, your AuditedCart subclass reports twice as many added items. Nobody touched your code. How?
Tests fragile base class, self-use, composition
Show answerHide answer
Model answer
That's the fragile base class problem. Your subclass overrides both add and addAll and counts in each. In the old version, Cart.addAll copied items directly into its list. In the new version, it loops and calls add for each item, which dispatches to your override. Every item is now counted twice: once in your addAll, once in your add.
class AuditedCart extends Cart {
private int added;
@Override public void add(String sku) { added++; super.add(sku); }
@Override public void addAll(Collection<String> skus) { added += skus.size(); super.addAll(skus); }
}
Your code depended on how the parent is implemented, not on what it promises. The fix is composition: wrap a Cart, forward calls, and count in your own methods, so internal self-use can't reach you.
How I’d say it in the interview
"The parent changed its self-use: addAll now calls add, which is overridable, so my override counts each item again. Inheritance coupled me to the parent's internals. I'd switch to a wrapper that forwards to the Cart, or ask the library to document self-use or make the class final."
Signals depth
Citing Effective Java Items 18 and 19: favour composition, and design and document for inheritance (with @implSpec) or prohibit it.
Red flags
"The library has a bug." It may not: nothing in its contract said addAll wouldn't call add.
Follow-ups interviewers ask next
What if you can't change the code and need a fix today?
Remove the counting from your
addAlloverride only if the new version is pinned and documented to calladd, and add a test that asserts the count foraddAll. It's a tactical fix; the wrapper is the real one.What if you own
Cartand want to prevent this for everyone?Make it
finalorsealed, or document the self-use in@implSpecand expose a deliberateprotectedhook such asonItemAdded, calling it from one place.
This code has a growing chain of instanceof checks for payment types. How would you refactor it?
Tests polymorphism, sealed types, exhaustive switch
Show answerHide answer
Model answer
long fee(Payment p) {
if (p instanceof CardPayment c) return c.amountPaise() * 2 / 100;
if (p instanceof UpiPayment) return 0;
throw new IllegalStateException("unknown payment " + p);
}
The problem is that the compiler can't tell you the chain is incomplete. Add WalletPayment and this fails at runtime, in whichever method nobody updated.
If the behaviour belongs to the type, make fee() an abstract method and let each class implement it. If it's an outside concern (reporting, mapping to a DTO), make Payment a sealed interface and use an exhaustive switch with no default. Adding a permitted type then breaks the build at every switch that doesn't handle it.
How I’d say it in the interview
"An instanceof chain over our own types usually means a missing polymorphic method. If the logic belongs in the domain, I'd add an abstract method. If not, a sealed interface with a pattern switch gives me exhaustiveness checking in Java 21, so a new type is a compile error instead of a production exception."
Signals depth
Knowing when instanceof is still right: equals, optional capabilities such as RandomAccess, and adapting third-party types you can't modify. And noting that a default branch in the switch throws away the exhaustiveness check.
Red flags
Proposing a Map<Class<?>, Function> registry for three types, or "add an else branch that logs".
Follow-ups interviewers ask next
What if the types come from a third-party library you can't seal?
Wrap them at the boundary: convert to your own sealed hierarchy in one adapter, and keep the
instanceofchecks there, in a single place with tests.
You added a third payment type, which is only 2% of traffic, and the checkout hot path got noticeably slower in load tests. Why?
Tests inline caches, megamorphic call sites, JIT
Show answerHide answer
Model answer
The call payment.fee() in the hot loop was probably bimorphic before: the JIT had seen only CardPayment and UpiPayment, inlined both behind a class check, and then optimised across the inlined code. A third receiver class makes the site megamorphic. If no single class reaches about 90% of calls, C2 stops inlining and emits a real vtable or itable call, which also blocks escape analysis and other optimisations around it.
For small methods that's commonly a 2x to 5x slowdown of that call, which shows up when it sits in a tight loop.
How I’d say it in the interview
"The call site went from bimorphic to megamorphic, so the JIT lost inlining. I'd confirm with a profiler and -XX:+PrintInlining, then reduce polymorphism at that one site, for example by making the fee a data field or handling the dominant type explicitly."
Signals depth
Knowing the default type profile width (2) and that benchmarks which only feed one type can hide this, so JMH tests should use a production-like type mix.
Red flags
"Add final to all the classes." That doesn't make a megamorphic site monomorphic.
Follow-ups interviewers ask next
What if the slowdown doesn't show up in the microbenchmark?
The benchmark probably exercises one type, so the site stays monomorphic. Feed it the real distribution of
CardPayment,UpiPaymentandWalletPaymentand the gap appears.
A service started failing with NoClassDefFoundError on a line that only does if (event instanceof KafkaEvent). The event isn't a Kafka event. What's going on?
Tests instanceof bytecode, class resolution, optional dependencies
Show answerHide answer
Model answer
The instanceof bytecode has to resolve the class it names before it can compare anything. The JVM does that the first time the instruction runs with a non-null reference. If KafkaEvent isn't on the runtime classpath (an optional dependency that was dropped from this deployment, for example), resolution fails with NoClassDefFoundError, even though the answer would have been false.
It only appears at runtime, only on the first non-null check, and not in tests where the dependency is present.
How I’d say it in the interview
"instanceof doesn't return false for a missing class; it needs the class loaded to answer. With null it skips resolution and returns false, which is why it can hide in tests. I'd isolate the optional integration behind its own class that's only loaded when the dependency is present."
Signals depth
Knowing that null gives false without resolving, and that the fix pattern is the one Spring and other libraries use: guard with a ClassUtils.isPresent-style check and keep references to optional classes inside a separate class.
Red flags
"instanceof can't throw." It can't throw ClassCastException or NullPointerException, but linkage errors are another matter.
Follow-ups interviewers ask next
What if the class exists but was loaded by a different class loader than the event's class?
Then there's no error, and the answer is
falseeven though the names match, because the JVM treats them as two different types. You'll see this in plugin systems and app servers.
What are covariant return types, and how does the JVM handle them?
Tests covariant returns, bridge methods, erasure
Show answerHide answer
Model answer
Since Java 5, an override may return a subtype of the parent's return type: Payment copy() can be overridden by CardPayment copy(). Callers holding a CardPayment get a CardPayment back without a cast.
The JVM, though, identifies methods by name and full descriptor, including the return type, so copy()Payment and copy()CardPayment are different methods to it. javac generates a synthetic bridge method Payment copy() in CardPayment that calls the covariant one, so calls through a Payment reference still dispatch correctly. The same trick makes generic overrides like compareTo(Order) work after erasure.
How I’d say it in the interview
"An override can narrow the return type. Because the JVM matches on full descriptors, javac adds a bridge method with the old return type that delegates to the new one. Reflection shows both, and isBridge() tells them apart."
Signals depth
Mentioning that covariance applies to reference types only (an int return can't become short), and that bridge methods explain an extra frame in stack traces and duplicate entries from getDeclaredMethods().
Red flags
"The return type can be anything in an override" or "covariant returns mean the parameter types can change".
Follow-ups interviewers ask next
What if a framework scans methods by name with reflection and processes each one?
It may process
copytwice, once for the bridge. Filter with!method.isBridge(), which is what mature frameworks do.