Method overriding lets a subclass replace a parent method's implementation at runtime
The JVM uses dynamic dispatch: calls go to the actual object type, not the reference type
Five strict rules: same signature, covariant returns, wider access, narrower exceptions, instance-only
Without @Override, a typo silently creates a new method — polymorphic calls hit the parent
Overriding is how frameworks like Spring inject proxy behaviour via AOP and decorators
Plain-English First
Imagine your family has a recipe for chocolate cake passed down from your grandma. You follow it mostly, but you swap regular flour for almond flour because you prefer it. You didn't throw away the original recipe — you just made your own version of it. Method overriding is exactly that: a child class takes a method it inherited from its parent and rewrites it to behave differently, while keeping the same name and purpose.
Every serious Java codebase relies on polymorphism, and method overriding is the engine that makes polymorphism actually work at runtime. Without it, you'd have to write a separate method call for every single subtype you ever create — and every time you added a new one, you'd have to go back and update that code. That's the kind of brittleness that causes midnight production fires.
What Method Overriding Actually Is (and Why Java Needs It)
Method overriding happens when a subclass declares a method with the exact same name, return type, and parameter list as a method in its parent class. At runtime, Java's JVM calls the subclass version — not the parent's — even if the reference variable is typed as the parent. This is called dynamic dispatch, and it's the backbone of runtime polymorphism.
The reason Java needs this is straightforward: you want to write code that works with a general type (say, a PaymentProcessor) without caring whether the concrete object underneath is a CreditCardProcessor, PayPalProcessor, or CryptoProcessor. Each subclass overrides the same method (processPayment) with its own logic, and your calling code stays untouched when you add a new payment type. That's the open/closed principle in action.
Without overriding, every method call on a parent reference would always execute the parent's logic, making inheritance far less useful. Overriding is what gives subclasses their own voice.
PaymentProcessorDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// PaymentProcessorDemo.java// Real-world scenario: a payment system that supports multiple payment methods.// The calling code (checkout) never changes when we add a new payment type.classPaymentProcessor {
// Parent method — defines the contract.// Every payment processor CAN process a payment, but HOW is up to the subclass.publicStringprocessPayment(double amount) {
return"Processing $" + amount + " via generic payment processor.";
}
publicStringgetProcessorName() {
return"Generic Processor";
}
}
classCreditCardProcessorextendsPaymentProcessor {
// @Override tells the compiler: "I'm intentionally replacing the parent method."// If we typo the method name, the compiler catches it immediately.
@OverridepublicStringprocessPayment(double amount) {
// Subclass provides its own implementation — completely different logic.return"Charging $" + amount + " to credit card via Stripe gateway.";
}
@OverridepublicStringgetProcessorName() {
return"Credit Card Processor";
}
}
classPayPalProcessorextendsPaymentProcessor {
@OverridepublicStringprocessPayment(double amount) {
return"Sending $" + amount + " via PayPal. Redirecting to paypal.com...";
}
@OverridepublicStringgetProcessorName() {
return"PayPal Processor";
}
}
classCryptoProcessorextendsPaymentProcessor {
privateString walletAddress;
publicCryptoProcessor(String walletAddress) {
this.walletAddress = walletAddress;
}
@OverridepublicStringprocessPayment(double amount) {
// Subclass can use its own fields while still honouring the parent's contract.return"Transferring $" + amount + " in BTC to wallet: " + walletAddress;
}
@OverridepublicStringgetProcessorName() {
return"Crypto Processor";
}
}
publicclassPaymentProcessorDemo {
// This method accepts ANY PaymentProcessor — it doesn't know or care which one.// This is the power of overriding: the calling code is completely decoupled.publicstaticvoidcheckout(PaymentProcessor processor, double orderTotal) {
System.out.println("--- Checkout using: " + processor.getProcessorName() + " ---");
// Java resolves the correct processPayment() at RUNTIME based on actual object type.String result = processor.processPayment(orderTotal);
System.out.println(result);
System.out.println();
}
publicstaticvoidmain(String[] args) {
double orderTotal = 149.99;
// Same checkout() call — different behaviour each time. That's runtime polymorphism.checkout(newCreditCardProcessor(), orderTotal);
checkout(newPayPalProcessor(), orderTotal);
checkout(newCryptoProcessor("1A2B3C4D5E6F"), orderTotal);
// Even using a parent-type reference, Java dispatches to the subclass method.PaymentProcessor unknownProcessor = newPayPalProcessor();
System.out.println("Reference type: PaymentProcessor");
System.out.println("Actual object: PayPalProcessor");
System.out.println(unknownProcessor.processPayment(50.00));
}
}
Output
--- Checkout using: Credit Card Processor ---
Charging $149.99 to credit card via Stripe gateway.
--- Checkout using: PayPal Processor ---
Sending $149.99 via PayPal. Redirecting to paypal.com...
--- Checkout using: Crypto Processor ---
Transferring $149.99 in BTC to wallet: 1A2B3C4D5E6F
Reference type: PaymentProcessor
Actual object: PayPalProcessor
Sending $50.0 via PayPal. Redirecting to paypal.com...
Always Use @Override
The @Override annotation isn't optional decoration — it's a safety net. If you misspell the method name or get the signature slightly wrong, the compiler throws an error immediately instead of silently creating a brand-new method that never gets called. Make it a habit you never skip.
Production Insight
In production, framework proxies (Spring AOP, Hibernate) generate subclasses at runtime.
Without @Override, the framework's generated code may not register your custom logic.
Always annotate overrides even in framework callbacks — the annotation signals intent to both the compiler and the developer.
Key Takeaway
Overriding = runtime dispatch via actual object type, not reference type.
@Override is a compile-time guard against silent bugs.
Always annotate every overriding method — no exceptions.
The Five Rules of Method Overriding You Must Know Cold
Overriding isn't a free-for-all. Java enforces five strict rules, and violating any one of them causes a compile-time error or silent misbehaviour.
Rule 1 — Same method signature: The method name and parameter list must be identical. Even a different parameter type creates a completely separate method (that's overloading, not overriding).
Rule 2 — Covariant return types: The overriding method's return type must be the same as, or a subtype of, the parent's return type. A parent returning Animal can be overridden to return Dog, because Dog is an Animal.
Rule 3 — Access modifier can only widen: If the parent method is protected, the child can make it protected or public, but never private. You can't reduce visibility — that would break code relying on the parent's contract.
Rule 4 — No new checked exceptions: The overriding method can't throw checked exceptions broader than the parent's declared exceptions. It can throw fewer, narrower, or none at all. Unchecked exceptions have no restriction.
Rule 5 — Only instance methods: Static methods belong to the class, not an instance, so they can't be overridden. They can be hidden (a different thing entirely), but that doesn't trigger polymorphic dispatch.
OverridingRulesDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// OverridingRulesDemo.java// Demonstrates the five overriding rules with concrete, runnable examples.import java.io.IOException;
classAnimal {
// Rule 1: Subclass must match this exact signature.publicStringdescribe() {
return"I am a generic animal.";
}
// Rule 2: Returns Animal — subclass may return Animal or any subtype (e.g., Dog).publicAnimalreproduce() {
returnnewAnimal();
}
// Rule 3: protected — subclass may use protected or public, NOT private.protectedStringgetHabitat() {
return"Unknown habitat";
}
// Rule 4: Declares IOException — subclass cannot throw a broader checked exception.publicvoidloadFromFile() throwsIOException {
System.out.println("Animal loading from file.");
}
// Rule 5: static methods belong to the class — NOT polymorphically overridable.publicstaticStringkingdom() {
return"Animalia (from Animal class)";
}
}
classDogextendsAnimal {
// Rule 1 ✅ — identical signature.
@OverridepublicStringdescribe() {
return"I am a dog. I bark.";
}
// Rule 2 ✅ — covariant return: Dog is a subtype of Animal. Perfectly valid.
@OverridepublicDogreproduce() {
return new Dog(); // More specific return type — still satisfies the contract.
}
// Rule 3 ✅ — widening access from protected to public is allowed.
@OverridepublicStringgetHabitat() {
return"Domestic home";
}
// Rule 4 ✅ — throwing NO exception is narrower than IOException. Totally fine.
@OverridepublicvoidloadFromFile() {
System.out.println("Dog loading from file — no exception declared.");
}
// Rule 5 — this is METHOD HIDING, not overriding.// The reference type determines which static method is called, not the object type.publicstaticStringkingdom() {
return"Animalia (from Dog class)";
}
}
publicclassOverridingRulesDemo {
publicstaticvoidmain(String[] args) throwsIOException {
// --- Rule 1 & 2 & 3 in action ---Animal myAnimal = new Dog(); // Parent reference, Dog object.// Runtime dispatch picks Dog's describe() — overriding is working.System.out.println(myAnimal.describe());
// Runtime dispatch picks Dog's getHabitat() — access widening worked.System.out.println(myAnimal.getHabitat());
// Covariant return: calling through Animal reference returns Animal.// Actual object returned is a Dog — checked with instanceof.Animal offspring = myAnimal.reproduce();
System.out.println("Offspring is Dog? " + (offspring instanceofDog));
// --- Rule 4 in action ---
myAnimal.loadFromFile(); // Calls Dog's version — no exception thrown.// --- Rule 5: Static method hiding — NOT polymorphism ---Animal animalRef = newDog();
Dog dogRef = newDog();
// Static calls resolved at COMPILE TIME using the reference type — not the object.System.out.println(animalRef.kingdom()); // Uses Animal's kingdom()System.out.println(dogRef.kingdom()); // Uses Dog's kingdom()System.out.println(Animal.kingdom()); // Preferred way to call static methods.
}
}
Output
I am a dog. I bark.
Domestic home
Offspring is Dog? true
Dog loading from file — no exception declared.
Animalia (from Animal class)
Animalia (from Dog class)
Animalia (from Animal class)
Watch Out: Static Methods Are NOT Overridden
If you define a static method with the same signature in a subclass, Java doesn't override it — it hides it. Calling it through a parent reference still calls the parent's version. Many developers get bitten by this when they mistakenly expect polymorphic behaviour from static methods. If you need polymorphism, the method must be an instance method.
Production Insight
Rule violations often cause silent bugs in production. Static method hiding is the most common — devs see the child's static method in their own tests but production code uses the parent reference.
Checked exception rule breaches cause cryptic compile errors when upgrading libraries — usually when a child class used to not declare an exception but a parent introduced one.
Access narrowing is caught at compile time, but accidentally marking a method private in a subclass thinking it's safe leads to unexpected behaviour when the method isn't actually overridden.
Key Takeaway
Five rules: same signature, covariant return, wider access, narrower exceptions, instance only.
Break any rule and you get a compile error or non-polymorphic behaviour — never silent success.
Memorise them: interviewers love asking which rules allow what changes.
Calling the Parent's Logic With super — and When You Actually Should
Sometimes you don't want to completely replace the parent's behaviour — you want to extend it. That's where super.methodName() comes in. It lets the overriding method call the parent's version first (or last, or anywhere in between) and then add its own logic on top.
A classic real-world example is logging or auditing. Your base class might handle the core transaction logic, and each subclass calls super.processTransaction() to get that for free, then adds its own extra steps like fraud detection or currency conversion.
But there's an important nuance: use super when the parent's logic is genuinely reusable and correct for all subclasses. If you find yourself calling super and then immediately undoing half of what it did, that's a design smell — the parent method is probably trying to do too much. In that case, refactor into smaller methods rather than fighting the inheritance chain.
You can only call super.method() one level up — you can't chain super.super.method() in Java. If you need grandparent behaviour, rethink your hierarchy.
TransactionAuditDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// TransactionAuditDemo.java// Real-world scenario: a bank transaction system where the base class handles// the core transfer, and subclasses add their own specialised behaviour on top.classBankTransaction {
protectedString accountId;
protecteddouble balance;
publicBankTransaction(String accountId, double initialBalance) {
this.accountId = accountId;
this.balance = initialBalance;
}
// Core transaction logic that ALL subclasses benefit from.publicvoidprocessTransaction(double amount) {
if (amount <= 0) {
thrownewIllegalArgumentException("Transaction amount must be positive.");
}
balance -= amount;
// Every subclass gets this audit trail for free via super.System.out.println("[BASE AUDIT] Account " + accountId +
" | Debited: $" + amount +
" | New Balance: $" + balance);
}
}
classInternationalTransactionextendsBankTransaction {
private static final double FX_FEE_PERCENT = 0.025; // 2.5% foreign exchange feepublicInternationalTransaction(String accountId, double initialBalance) {
super(accountId, initialBalance);
}
@OverridepublicvoidprocessTransaction(double amount) {
double fxFee = amount * FX_FEE_PERCENT;
double totalCharge = amount + fxFee;
System.out.println("[INTERNATIONAL] Applying FX fee of $" +
String.format("%.2f", fxFee) + " on transfer of $" + amount);
// Call the parent's logic to handle the actual debit and audit trail.// We don't duplicate that logic — we reuse it.super.processTransaction(totalCharge);
System.out.println("[INTERNATIONAL] Transfer complete. Total charged: $" +
String.format("%.2f", totalCharge));
}
}
classFraudCheckedTransactionextendsBankTransaction {
privatestaticfinaldouble FRAUD_THRESHOLD = 10_000.00;
publicFraudCheckedTransaction(String accountId, double initialBalance) {
super(accountId, initialBalance);
}
@OverridepublicvoidprocessTransaction(double amount) {
// Pre-processing step: run fraud check BEFORE calling the parent.if (amount > FRAUD_THRESHOLD) {
System.out.println("[FRAUD ALERT] Transaction of $" + amount +
" exceeds threshold. Flagging for manual review.");
// We deliberately do NOT call super here — fraud check blocks the transaction.return;
}
System.out.println("[FRAUD CHECK] Amount $" + amount + " passed screening.");
// Safe to proceed — delegate to parent for the actual debit.super.processTransaction(amount);
}
}
publicclassTransactionAuditDemo {
publicstaticvoidmain(String[] args) {
System.out.println("=== International Transaction ===");
InternationalTransaction intlTx =
newInternationalTransaction("ACC-001", 5000.00);
intlTx.processTransaction(1000.00);
System.out.println();
System.out.println("=== Fraud-Checked Transaction (safe amount) ===");
FraudCheckedTransaction fraudTx =
newFraudCheckedTransaction("ACC-002", 20000.00);
fraudTx.processTransaction(500.00);
System.out.println();
System.out.println("=== Fraud-Checked Transaction (suspicious amount) ===");
fraudTx.processTransaction(15000.00);
}
}
Output
=== International Transaction ===
[INTERNATIONAL] Applying FX fee of $25.00 on transfer of $1000.0
[FRAUD ALERT] Transaction of $15000.0 exceeds threshold. Flagging for manual review.
Interview Gold: super() vs super.method()
super() (no dot, with parentheses) calls the parent constructor and must be the first line in a constructor. super.methodName() calls a specific parent instance method and can appear anywhere in the overriding method. Interviewers love testing whether candidates know this distinction cold.
Production Insight
In production, super calls are a common source of infinite loops when overriding methods in event listeners or callbacks — the super call may trigger the same event again.
Always verify that the parent method doesn't invoke the same method on this (which would call the child's override recursively). This is the template method pattern trap.
If you see a StackOverflowError in a subclass with a super call, you've likely hit this.
Key Takeaway
Use super.method() to extend parent behaviour — but only when parent logic is genuinely reusable.
If you're undoing parent's work, refactor. No super.super.method() — fix the hierarchy.
super() (constructor) vs super.method() — know the difference cold for interviews.
Overriding vs Overloading — The Difference That Trips Everyone Up
These two terms sound similar enough to cause real confusion, and conflating them in an interview is a red flag. Here's the clearest mental model: overriding is about replacing behaviour across class levels (parent vs child). Overloading is about adding behaviour within the same class level by creating multiple methods with the same name but different parameters.
Overriding is resolved at runtime — the JVM looks at the actual object in memory to decide which method to call. Overloading is resolved at compile time — the compiler picks the right version based on the argument types you pass.
Practically, this means overriding is a subclass concern and overloading is a single-class concern. You can combine them (a subclass can overload AND override methods), but they're fundamentally different mechanisms serving different purposes. The comparison table below captures the key distinctions at a glance.
OverrideVsOverloadDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// OverrideVsOverloadDemo.java// Side-by-side demo: overloading in the same class, overriding across class hierarchy.classNotificationService {
// --- OVERLOADING: same class, same method name, different parameter lists ---// The compiler picks the right one based on what arguments you pass.publicvoidsendAlert(String message) {
// Overload 1: just a plain message string.System.out.println("[EMAIL] Sending alert: " + message);
}
publicvoidsendAlert(String message, String recipientEmail) {
// Overload 2: message + a specific email address.System.out.println("[EMAIL] Sending '" + message + "' to " + recipientEmail);
}
publicvoidsendAlert(String message, int priorityLevel) {
// Overload 3: message + numeric priority. Same name, different signature.System.out.println("[EMAIL] Priority " + priorityLevel + " alert: " + message);
}
// --- Method to be OVERRIDDEN by subclasses ---publicStringgetChannel() {
return"Email";
}
}
classSmsNotificationServiceextendsNotificationService {
// --- OVERRIDING: subclass redefines parent's method. Resolved at runtime. ---
@OverridepublicStringgetChannel() {
return "SMS"; // Completely replaces the parent's "Email" response.
}
// Subclass can ALSO overload — adds a new overload specific to SMS.publicvoidsendAlert(String message, String phoneNumber, boolean isUrgent) {
String urgencyTag = isUrgent ? "[URGENT] " : "";
System.out.println("[SMS] Sending '" + urgencyTag + message +
"' to " + phoneNumber);
}
}
publicclassOverrideVsOverloadDemo {
publicstaticvoidmain(String[] args) {
System.out.println("--- Overloading: resolved at compile time ---");
NotificationService emailService = newNotificationService();
// Compiler picks the correct overload based on argument types.
emailService.sendAlert("Server is down"); // Overload 1
emailService.sendAlert("Server is down", "admin@company.com"); // Overload 2
emailService.sendAlert("Server is down", 1); // Overload 3System.out.println();
System.out.println("--- Overriding: resolved at runtime ---");
// Parent reference, but the actual object is SmsNotificationService.NotificationService service = newSmsNotificationService();
// At runtime, JVM sees the real object is SmsNotificationService// and calls ITS getChannel() — not NotificationService's.System.out.println("Notification channel: " + service.getChannel());
System.out.println();
System.out.println("--- SMS-specific overload (only on SmsNotificationService) ---");
SmsNotificationService smsService = newSmsNotificationService();
smsService.sendAlert("Payment confirmed", "+1-555-0199", false);
smsService.sendAlert("Fraud detected", "+1-555-0199", true);
}
}
Output
--- Overloading: resolved at compile time ---
[EMAIL] Sending alert: Server is down
[EMAIL] Sending 'Server is down' to admin@company.com
[EMAIL] Priority 1 alert: Server is down
--- Overriding: resolved at runtime ---
Notification channel: SMS
--- SMS-specific overload (only on SmsNotificationService) ---
[SMS] Sending 'Payment confirmed' to +1-555-0199
[SMS] Sending '[URGENT] Fraud detected' to +1-555-0199
The One-Line Memory Hook
Overriding = runtime, subclass replaces parent. Overloading = compile time, same class adds variants. If you can say that in your sleep, you'll nail every interview question that mixes the two concepts.
Production Insight
A common production bug: a developer adds a toString() override in a POJO but accidentally types toString(String prefix) — that's overloading, not overriding. The logger still calls the original toString() and the POJO prints its object hash.
This happens often when you mean to override a method from a superclass (like equals, hashCode) but get the signature wrong.
If logging shows unexpected values, always verify the method signature with @Override.
When in doubt, add @Override — if it compiles, you're overriding. If not, you're overloading.
When to Prefer Composition Over Inheritance (and Overriding)
Inheritance and overriding are powerful, but they come with tight coupling. A change in the parent class can silently affect all subclasses. If you override a method today, you're locking yourself into the parent's contract — and if the parent changes its behaviour in a future release, your override may break.
This is why many senior engineers reach for composition before inheritance. Instead of making PaymentService extend BaseService, you inject a PaymentGateway into PaymentService. The behaviour is defined by an interface, and you change it by swapping implementations — no overriding needed.
When should you still use overriding? When the subclass truly IS a kind of the parent (is-a relationship), and the parent method's contract is stable. Classic examples: HashMap extends AbstractMap, ArrayList extends AbstractList. Core API classes rarely change their contracts.
But for your own business logic, prefer interfaces + composition. You get flexibility without the fragile base class problem. Override only when you're absolutely sure the parent won't change and you need polymorphic dispatch at runtime.
The Fragile Base Class Problem
Parent adds a new call to an existing method internally, triggering behaviour in subclasses that wasn't there before.
Subclass's super call may now do something the subclass didn't expect.
Unit tests on subclasses don't catch parent-side changes — they run against the subclass, not the parent.
Solution: prefer composition unless you control both parent and child and the contract is frozen.
Production Insight
A real-world case: a library updated its validate() method in the parent class to call a new checkPendingTransactions() method. Subclasses that overrode validate() and called super were now performing database queries they never intended — causing a 4-second latency spike.
The dependency was on a third-party library; the only fix was to rewrite the subclass without inheritance.
This is why production codebases often ban inheritance across module boundaries unless absolutely necessary.
Key Takeaway
Prefer composition over inheritance for flexibility.
Use overriding only when you truly have an is-a relationship and parent contracts are stable.
The fragile base class problem is real — it's why many production codebases ban deep inheritance hierarchies.
Should you use overriding or composition?
IfYou control both parent and child, and the contract is stable
→
UseInheritance + overriding is safe — use it for shared behaviour that must be polymorphic.
IfThe parent is from a third-party library or likely to change
→
UseAvoid inheritance. Use composition with an interface and delegate via a field.
IfYou only need to reuse behaviour, not model an is-a relationship
→
UseUse composition. Overriding is about substitution, not reuse.
IfYou need runtime polymorphism (same reference type, different behaviour)
→
UseOverriding is the only way — but prefer interfaces over abstract classes for flexibility.
● Production incidentPOST-MORTEMseverity: high
The Missing @Override That Lost a Payment
Symptom
In production, credit card payments logged as 'Processing $149.99 via generic payment processor' instead of the actual gateway response. Summed thousands of pending transactions flagged by finance.
Assumption
The developer assumed the method was overriding because the name matched. The intent was clear to anyone reading the code.
Root cause
The child class method was declared processPayment(double amount, String currency) while the parent had processPayment(double amount). With no @Override annotation, Java treated it as a new overload — the existing polymorphic calls still resolved to the parent.
Fix
Added @Override annotation to all overriding methods in the codebase. Compiled immediately caught 12 more mismatched signatures that had gone unnoticed for weeks.
Key lesson
Always annotate overrides with @Override — it's not optional decoration, it's a compile-time safety net.
Code review cannot catch signature mismatches if the reviewer assumes intent. Only the compiler can guarantee correctness.
Without @Override, a single typo in a parameter type can cause polymorphic dispatch to silently call the wrong method — no warning, no error, just wrong behaviour.
Production debug guideQuick symptom-to-action guide for when polymorphic calls don't behave as expected4 entries
Symptom · 01
A method call on a parent reference executes the parent's logic instead of the child's
→
Fix
Check the child method signature matches exactly (name + parameter types). Look for missing @Override — add it and recompile to detect mismatches.
Symptom · 02
A static method is being called polymorphically (via instance reference) and behaves differently than expected
→
Fix
Static methods are hidden, not overridden. Change to instance method or use the class name explicitly to call the intended version.
Symptom · 03
A private method with the same name in child class is never called from parent-reference code
→
Fix
Private methods are invisible to subclasses. Change access to protected or public if overriding is needed, or redesign the hierarchy.
Symptom · 04
Overriding method throws an unexpected checked exception at runtime
→
Fix
The child method cannot throw a broader checked exception than the parent. If the parent doesn't declare the exception, the child must catch it internally or declare a narrower exception.
★ Quick Debug Cheat SheetImmediate commands and checks when method overriding breaks in production
Method call doesn't use child's implementation−
Immediate action
Add @Override to the child method and recompile — compiler error indicates mismatch
Commands
javac -Xlint:all OverridingRulesDemo.java
javap -c -p ParentClass ChildClass
Fix now
Correct the signature to match parent exactly, then add @Override and recompile.
Static method behaves non-polymorphically+
Immediate action
Verify the method is not static. If it must be static, ensure callers use the subclass type reference.
Commands
javap -c -p -verbose SubClass | grep 'static'
Change to instance method and recompile
Fix now
Remove static keyword from the method definition in the subclass; call through instance reference.
Private method not available for overriding+
Immediate action
Change access modifier from private to protected or public in parent class
Commands
grep -r 'private.*processPayment' src/
Refactor: promote the method to protected in parent, then override in child with @Override.
Fix now
Declare the parent method as protected instead of private, then add override in subclass.
Feature / Aspect
Method Overriding
Method Overloading
Where it happens
Across parent and child classes
Within the same class
Method signature
Must be identical (name + params)
Same name, different params
Return type
Same or covariant subtype
Can differ freely
Resolution time
Runtime (dynamic dispatch)
Compile time (static binding)
@Override annotation
Required best practice
Not applicable
Polymorphism type
Runtime polymorphism
Compile-time polymorphism
Access modifier
Can only widen (e.g., protected → public)
No restriction
Checked exceptions
Cannot throw broader exceptions
No restriction
Static methods
Cannot be overridden (only hidden)
Can be overloaded
Primary use case
Customise inherited behaviour per subtype
Provide multiple method signatures
Key takeaways
1
Method overriding is resolved at runtime by the JVM based on the actual object type
not the reference type. This is the mechanism that makes runtime polymorphism possible in Java.
2
Always annotate overriding methods with @Override. It costs nothing and buys you compile-time safety against signature typos that would otherwise silently create a new method instead of overriding.
3
You can extend a parent's behaviour without duplicating it by calling super.methodName() inside the override
but only use super when the parent logic is genuinely reusable, not as a workaround for a poor class design.
4
Static methods and private methods cannot be overridden. Static methods are hidden (compile-time resolved by reference type), and private methods are invisible to subclasses entirely. Neither participates in dynamic dispatch.
5
Prefer composition over inheritance unless you have a clear is-a relationship and the parent contract is stable. Overriding comes with tight coupling and the fragile base class problem.
Common mistakes to avoid
4 patterns
×
Forgetting @Override and accidentally overloading instead of overriding
Symptom
Your method has the same name but different parameters or return type. Polymorphic calls still invoke the parent's version — your subclass method is never called. No compiler warning because it's a valid overload.
Fix
Always add @Override to every method you intend to override. The compiler will catch any signature mismatch immediately.
×
Trying to override a private or static parent method
Symptom
Writing a method with the same name in the child class compiles fine, but calling through a parent reference always executes the parent version. Static methods are hidden, not overridden; private methods are invisible.
Fix
Change private to protected or public if you genuinely need overriding. For static methods, use instance methods or call the method via the child class name explicitly.
×
Narrowing the access modifier in the overriding method
Symptom
Changing a parent's public method to protected (or private) in the child causes a compile-time error: 'attempting to assign weaker access privileges'.
Fix
Keep the access level the same or widen it. You can go from protected to public, never the other way.
×
Expecting covariant return types to work with generics without understanding type erasure
Symptom
You override a method returning List<Number> with a method returning List<Integer> and get a compile error. The generics are erased, so the return type is seen as List vs List, which conflicts.
Fix
Covariant return types work only when the erasure is compatible. Use wildcards or additional type parameters to achieve the desired variance.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
What is the difference between method overriding and method hiding in Ja...
Q02SENIOR
Can you override a method and throw a new checked exception that the par...
Q03SENIOR
If a parent class method is marked final, what happens when a subclass t...
Q04SENIOR
What is the Liskov Substitution Principle and how does it relate to meth...
Q05JUNIOR
How does the `super` keyword work in the context of method overriding? C...
Q01 of 05SENIOR
What is the difference between method overriding and method hiding in Java, and how does each behave when called through a parent-type reference?
ANSWER
Method overriding applies to instance methods: the subclass defines a method with the same signature, and at runtime the JVM calls the child's version based on the actual object type (dynamic dispatch). Method hiding applies to static methods: the subclass defines a static method with the same signature, but at compile time the reference type determines which version is called. So calling Parent.staticMethod() always uses the parent's, even if the variable holds a child instance. Overriding gives polymorphism; hiding does not.
Q02 of 05SENIOR
Can you override a method and throw a new checked exception that the parent method doesn't declare? What about unchecked exceptions?
ANSWER
No for checked exceptions — the overriding method cannot throw a broader checked exception than the parent declares. It can throw fewer, narrower, or none at all. Unchecked exceptions (RuntimeException and subclasses) have no restriction — you can throw any runtime exception whether the parent declares it or not. This is because checked exceptions are part of the method contract and the Liskov Substitution Principle requires that the child doesn't impose stronger constraints on callers.
Q03 of 05SENIOR
If a parent class method is marked final, what happens when a subclass tries to override it — and why does Java allow final methods at all? Can you think of a real scenario where marking a method final is the right design decision?
ANSWER
If a method is final, a subclass cannot override it — the compiler gives an error. Java allows final methods for two main reasons: (1) security — a critical method like getClass() in Object must not be altered, and (2) immutability — in classes like String, many methods are final to guarantee consistent behaviour across all implementations. A real production scenario: a base class that loads configuration from a file may mark the loadConfig() method final because the loading order and validation must be invariant across all subclasses. Any subclass should add behaviour by overriding other non-final hooks, not the core loading method.
Q04 of 05SENIOR
What is the Liskov Substitution Principle and how does it relate to method overriding?
ANSWER
The Liskov Substitution Principle states that objects of a subclass should be substitutable for objects of the parent class without altering the correctness of the program. In the context of overriding, this means the overriding method must not violate the parent's contract: it cannot throw broader exceptions, cannot narrow access, and cannot change the expected behaviour in ways that break calling code. For example, if a parent's getBalance() returns a positive double, a subclass that overrides it to return a negative number would break LSP, even though the signature matches.
Q05 of 05JUNIOR
How does the `super` keyword work in the context of method overriding? Can you access a grandparent's method using `super.super.method()`?
ANSWER
super.methodName() calls the immediate parent's version of the method. You cannot use super.super.methodName() — Java does not support this. If you need grandparent behaviour, you must restructure the hierarchy, perhaps by extracting the grandparent's code into a separate method that both parent and child can call directly. The inability to bypass a parent's override is intentional — it prevents breaking encapsulation within the class hierarchy.
01
What is the difference between method overriding and method hiding in Java, and how does each behave when called through a parent-type reference?
SENIOR
02
Can you override a method and throw a new checked exception that the parent method doesn't declare? What about unchecked exceptions?
SENIOR
03
If a parent class method is marked final, what happens when a subclass tries to override it — and why does Java allow final methods at all? Can you think of a real scenario where marking a method final is the right design decision?
SENIOR
04
What is the Liskov Substitution Principle and how does it relate to method overriding?
SENIOR
05
How does the `super` keyword work in the context of method overriding? Can you access a grandparent's method using `super.super.method()`?
JUNIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
Can a constructor be overridden in Java?
No. Constructors are not inherited in Java, so they can't be overridden. Each class defines its own constructors. You can call a parent constructor using super() as the first line of a subclass constructor, but that's constructor chaining — not overriding.
Was this helpful?
02
What happens if I override equals() but not hashCode() in Java?
You'll break the contract that Java's collections rely on. If two objects are considered equal by your equals() override, they must have the same hashCode(). If you only override equals(), those equal objects may land in different hash buckets in a HashMap or HashSet and never be found again. Always override both together.
Was this helpful?
03
Is method overriding possible without inheritance?
No. Overriding fundamentally requires an inheritance relationship — a subclass redefining a method it inherited from a superclass or an interface it implements. Without inheritance or interface implementation, there's no parent method to override, and what you'd actually have is just a standalone method.
Was this helpful?
04
Can you override a method with a different return type?
Yes, only if the return type is a subtype (covariant return). The overriding method can return Dog when the parent returns Animal. But if the return types are unrelated, the compiler will flag it as an error — it's then overloading, not overriding.
Was this helpful?
05
What is the difference between overriding and overloading?
Overriding: same method name and parameter list across parent-child classes, resolved at runtime, @Override annotation applicable. Overloading: same method name but different parameters within the same class, resolved at compile time, no @Override needed. Both allow multiple methods with the same name but serve different purposes.