Java Method Overloading vs Overriding
Java Method Overloading vs Overriding
Overloading and overriding share a name prefix and both involve methods with the same name — and that surface similarity is exactly why beginners confuse them. They solve completely different problems, work at different stages of program execution, and belong to different types of polymorphism.
Overloading says: this class has multiple methods named calculate, each accepting different inputs. The compiler picks the right one before the program even runs. Overriding says: this subclass has its own version of makePayment that replaces the parent's version. The JVM picks the right one at runtime, after it knows what kind of object is actually being used.
Understanding the difference is not just interview preparation — it shapes how you design every class hierarchy you write.
What Is Method Overloading?
Method overloading is when a class defines two or more methods with the same name but different parameter lists. The methods can differ in the number of parameters, the types of parameters, or the order of parameter types. The return type alone cannot distinguish overloaded methods.
Overloading is resolved entirely at compile time. The compiler examines the argument types you pass and selects the matching method signature before generating bytecode. This is why overloading is called compile-time polymorphism or static polymorphism.
1// File: TaxCalculator.java
2
3public class TaxCalculator {
4
5 // Calculates GST for a basic amount
6 public double calculateTax(double amount) {
7 return amount * 0.18;
8 }
9
10 // Calculates tax with a custom rate
11 public double calculateTax(double amount, double rate) {
12 return amount * rate;
13 }
14
15 // Calculates tax for an integer quantity times unit price
16 public double calculateTax(int quantity, double unitPrice) {
17 return quantity * unitPrice * 0.18;
18 }
19
20 // Returns a formatted tax summary string
21 public String calculateTax(double amount, String category) {
22 double rate = category.equals("essential") ? 0.05 : 0.18;
23 return category + " tax on Rs." + amount + " = Rs." + (amount * rate);
24 }
25}1// File: OverloadingDemo.java
2
3public class OverloadingDemo {
4
5 public static void main(String[] args) {
6
7 TaxCalculator calc = new TaxCalculator();
8
9 // Compiler picks calculateTax(double) based on one double argument
10 System.out.println("GST: Rs." + calc.calculateTax(10000.0));
11
12 // Compiler picks calculateTax(double, double) — two doubles
13 System.out.println("Custom rate: Rs." + calc.calculateTax(10000.0, 0.12));
14
15 // Compiler picks calculateTax(int, double) — int then double
16 System.out.println("Qty tax: Rs." + calc.calculateTax(5, 2000.0));
17
18 // Compiler picks calculateTax(double, String) — double then String
19 System.out.println(calc.calculateTax(5000.0, "essential"));
20 }
21}Output:
GST: Rs.1800.0
Custom rate: Rs.1200.0
Qty tax: Rs.1800.0
essential tax on Rs.5000.0 = Rs.250.0
The method name calculateTax appears four times. The compiler resolves which version to call based on the number and types of arguments at each call site. No inheritance involved — overloading works within a single class.
What Is Method Overriding?
Method overriding is when a subclass provides its own implementation of a method that already exists in the parent class. The method name, parameter list, and return type must match the parent's declaration. The @Override annotation makes the intent explicit and lets the compiler catch mistakes.
Overriding is resolved at runtime. The JVM examines the actual type of the object at the moment of the call and dispatches to the most specific implementation in the class hierarchy. This is why overriding is called runtime polymorphism or dynamic dispatch.
1// File: PaymentProcessor.java
2
3public class PaymentProcessor {
4
5 // Parent defines the contract — every payment type must implement this
6 public String processPayment(double amount) {
7 return "Processing Rs." + amount + " via default channel.";
8 }
9
10 public String getProcessorName() {
11 return "Default Processor";
12 }
13}1// File: UpiPaymentProcessor.java
2
3public class UpiPaymentProcessor extends PaymentProcessor {
4
5 private final String upiId;
6
7 public UpiPaymentProcessor(String upiId) {
8 this.upiId = upiId;
9 }
10
11 // Overrides parent — JVM calls THIS version when object is UpiPaymentProcessor
12 @Override
13 public String processPayment(double amount) {
14 return "UPI transfer of Rs." + amount + " from " + upiId + " — initiated.";
15 }
16
17 @Override
18 public String getProcessorName() {
19 return "UPI Processor";
20 }
21}1// File: CardPaymentProcessor.java
2
3public class CardPaymentProcessor extends PaymentProcessor {
4
5 private final String maskedCard;
6
7 public CardPaymentProcessor(String maskedCard) {
8 this.maskedCard = maskedCard;
9 }
10
11 @Override
12 public String processPayment(double amount) {
13 return "Card charge of Rs." + amount + " on " + maskedCard + " — authorised.";
14 }
15
16 @Override
17 public String getProcessorName() {
18 return "Card Processor";
19 }
20}1// File: OverridingDemo.java
2
3import java.util.List;
4
5public class OverridingDemo {
6
7 public static void main(String[] args) {
8
9 // Reference type is PaymentProcessor — actual types vary
10 List<PaymentProcessor> processors = List.of(
11 new PaymentProcessor(),
12 new UpiPaymentProcessor("rahul@upi"),
13 new CardPaymentProcessor("**** **** **** 4521")
14 );
15
16 for (PaymentProcessor processor : processors) {
17 // JVM decides at runtime which processPayment() to call
18 System.out.println("[" + processor.getProcessorName() + "] "
19 + processor.processPayment(2499.0));
20 }
21 }
22}Output:
[Default Processor] Processing Rs.2499.0 via default channel.
[UPI Processor] UPI transfer of Rs.2499.0 from rahul@upi — initiated.
[Card Processor] Card charge of Rs.2499.0 on **** **** **** 4521 — authorised.
All three objects are referenced as PaymentProcessor. The for loop calls processPayment the same way on each. The JVM checks the actual object type at runtime and routes to the correct override. This is runtime polymorphism in action — the calling code does not change, but the behaviour does.
Overloading vs Overriding — Complete Comparison
| Aspect | Method Overloading | Method Overriding |
|---|---|---|
| Definition | Same method name, different parameter list in the same class | Same method name and signature in a subclass replacing the parent's version |
| Polymorphism type | Compile-time (static) polymorphism | Runtime (dynamic) polymorphism |
| Resolved at | Compile time — by the compiler | Runtime — by the JVM |
| Class relationship | Works within a single class (also works across parent-child) | Requires inheritance — parent and child classes |
| Parameter list | Must differ — type, count, or order | Must be identical |
| Return type | Can differ (does not alone distinguish overloads) | Must be same or a subtype (covariant return) |
| Access modifier | Can be anything | Cannot reduce visibility from parent |
@Override annotation | Not applicable | Strongly recommended — enforced by most teams |
static methods | Can be overloaded | Cannot be overridden — only hidden |
private methods | Can be overloaded | Cannot be overridden — not inherited |
final methods | Can be overloaded | Cannot be overridden |
| Exception handling | Can throw any exceptions | Cannot add new checked exceptions not declared in parent |
| When to use | Multiple ways to call the same logic with different inputs | Specialised behaviour in a subclass for an inherited method |
| Example | print(int), print(String), print(double) | Animal.sound() overridden by Dog.sound() |
How Compile-Time vs Runtime Resolution Works
The distinction between when each is resolved is what makes them fundamentally different — not just the rules about signatures.
When the compiler processes an overloaded call, it looks at the static type of the variable and the argument types in the call. It picks the best-matching method signature and records that choice in the bytecode. The program cannot change its mind later.
When the JVM processes an overriding call, it looks at the actual object sitting in memory at that moment. The reference type is ignored for dispatch purposes. Whatever concrete type the object actually is, that type's implementation runs.
Overloading — resolved by compiler:
Source code: calc.calculateTax(5, 2000.0)
| |
int double
|
Compiler matches to:
calculateTax(int, double)
|
Bytecode records this choice.
Cannot change at runtime.
Overriding — resolved by JVM at runtime:
Source code: processor.processPayment(2499.0)
|
Reference type: PaymentProcessor
|
Actual object: UpiPaymentProcessor
|
JVM looks up actual type's method table
|
Calls: UpiPaymentProcessor.processPayment()
This is why you can hold a PaymentProcessor reference and still get UPI-specific behaviour — the reference type controls what the compiler allows you to call, but the actual object type controls which implementation runs.
Overloading vs Overriding — Rules Summary Table
| Rule | Overloading | Overriding |
|---|---|---|
| Same method name | Required | Required |
| Same parameter list | Prohibited (at least one must differ) | Required (must match exactly) |
| Same return type | Not required | Required (or covariant subtype) |
| Inheritance needed | No | Yes |
| Works on static methods | Yes | No — static methods are hidden, not overridden |
| Works on private methods | Yes | No — private methods are not inherited |
| Works on final methods | Yes | No — final prevents overriding |
| Checked exception rules | No restrictions | Cannot declare new checked exceptions |
| Annotation | None required | @Override (strongly recommended) |
Real-World Example — Notification Service
The Business Problem
A notification service at a company like Swiggy or Meesho sends alerts through multiple channels: SMS, email, and push notifications. Every channel uses the same sendNotification concept, but each has its own delivery mechanism. Additionally, within the SMS channel, the same send operation needs to handle different inputs — just a message, a message with a scheduled time, or a message with a priority flag. Overriding handles the channel variation; overloading handles the input variation within each channel.
1// File: Notification.java
2
3public class Notification {
4
5 protected final String recipient;
6 protected final String message;
7
8 public Notification(String recipient, String message) {
9 this.recipient = recipient;
10 this.message = message;
11 }
12}1// File: NotificationSender.java
2
3public class NotificationSender {
4
5 // Overloaded — three ways to call send within this class
6 public void send(Notification notification) {
7 System.out.println("[SENDER] Dispatching to: " + notification.recipient);
8 }
9
10 public void send(Notification notification, long scheduledEpoch) {
11 System.out.println("[SENDER] Scheduled for epoch " + scheduledEpoch
12 + " to: " + notification.recipient);
13 }
14
15 public void send(Notification notification, int priorityLevel) {
16 System.out.println("[SENDER] Priority " + priorityLevel
17 + " dispatch to: " + notification.recipient);
18 }
19
20 // Overridden by subclasses — each channel has its own delivery logic
21 public String getChannelName() {
22 return "Generic";
23 }
24
25 public boolean deliver(Notification notification) {
26 System.out.println("[" + getChannelName() + "] Delivering: "
27 + notification.message + " → " + notification.recipient);
28 return true;
29 }
30}1// File: SmsNotificationSender.java
2
3public class SmsNotificationSender extends NotificationSender {
4
5 private final String smsGateway;
6
7 public SmsNotificationSender(String smsGateway) {
8 this.smsGateway = smsGateway;
9 }
10
11 @Override
12 public String getChannelName() {
13 return "SMS";
14 }
15
16 @Override
17 public boolean deliver(Notification notification) {
18 System.out.println("[SMS via " + smsGateway + "] Sending '"
19 + notification.message + "' to " + notification.recipient);
20 return true;
21 }
22}1// File: PushNotificationSender.java
2
3public class PushNotificationSender extends NotificationSender {
4
5 private final String appId;
6
7 public PushNotificationSender(String appId) {
8 this.appId = appId;
9 }
10
11 @Override
12 public String getChannelName() {
13 return "Push";
14 }
15
16 @Override
17 public boolean deliver(Notification notification) {
18 System.out.println("[Push App:" + appId + "] Sending '"
19 + notification.message + "' to device " + notification.recipient);
20 return true;
21 }
22}1// File: NotificationServiceDemo.java
2
3import java.util.List;
4
5public class NotificationServiceDemo {
6
7 public static void main(String[] args) {
8
9 Notification orderAlert = new Notification("9876543210", "Your order is out for delivery!");
10
11 NotificationSender smsSender = new SmsNotificationSender("Twilio-IN");
12 NotificationSender pushSender = new PushNotificationSender("com.meesho.app");
13
14 // Overloading in action — same sender, different call signatures
15 smsSender.send(orderAlert);
16 smsSender.send(orderAlert, 2); // priority overload
17 smsSender.send(orderAlert, 1735000000L); // scheduled overload
18
19 System.out.println();
20
21 // Overriding in action — same reference type, different runtime behaviour
22 List<NotificationSender> senders = List.of(smsSender, pushSender);
23 for (NotificationSender sender : senders) {
24 sender.deliver(orderAlert);
25 }
26 }
27}Output:
[SENDER] Dispatching to: 9876543210
[SENDER] Priority 2 dispatch to: 9876543210
[SENDER] Scheduled for epoch 1735000000 to: 9876543210
[SMS via Twilio-IN] Sending 'Your order is out for delivery!' to 9876543210
[Push App:com.meesho.app] Sending 'Your order is out for delivery!' to device 9876543210
The send calls on smsSender demonstrate overloading — three calls to send with different arguments, resolved by the compiler. The deliver calls in the loop demonstrate overriding — same reference type, different runtime objects, different output. Both mechanisms work together in the same example without interfering.
Best Practices
Use @Override on every overriding method without exception. A method that is meant to override but has a typo in the name — procesPayment instead of processPayment — silently becomes a new unrelated method without @Override. The annotation makes the compiler catch this mistake immediately.
Keep overloaded methods consistent in their core purpose. Overloading send(message) and send(message, priority) is sensible — the methods do the same thing with different levels of configuration. Overloading send(String message) and send(String filePath) is a code smell — two overloads with the same signature but completely different semantics confuse callers and lead to bugs.
Prefer composition and overriding for varying behaviour, overloading for varying inputs. When you find yourself writing if-else chains inside a method to handle different runtime object types, that is a sign that overriding should be doing the job. When you want to give callers flexibility in what they pass, overloading is the right tool.
Never override static methods and call them polymorphically. Static methods are resolved by reference type at compile time, not by object type at runtime. Placing @Override on a static method in a subclass creates method hiding — the parent version does not disappear, and the dispatch is not polymorphic. This is a common source of subtle bugs.
Common Mistakes
Mistake 1 — Trying to Overload by Return Type Only
1public class Calculator {
2
3 public int compute(int value) {
4 return value * 2;
5 }
6
7 // Compile error — return type alone does not distinguish overloads
8 public double compute(int value) {
9 return value * 2.0;
10 }
11}Compile error: compute(int) is already defined in Calculator
The compiler selects overloads based on argument types, not return type. If the parameter list is identical, the methods are considered duplicates regardless of what they return.
Mistake 2 — Thinking a Subclass Overloads the Parent's Method
1public class Base {
2 public void display(String message) {
3 System.out.println("Base: " + message);
4 }
5}
6
7public class Child extends Base {
8
9 // This is NOT overloading — it is a new method in Child
10 // display(String) from Base is still inherited unchanged
11 public void display(int code) {
12 System.out.println("Child code: " + code);
13 }
14}Child now has two display methods: the inherited display(String) from Base and its own display(int). This is technically an overload across the inheritance hierarchy, but Base.display(String) is not affected. A beginner expecting this to change the parent method's behaviour will be surprised.
Mistake 3 — Reducing Access Modifier in an Override
1public class Service {
2 public String getStatus() {
3 return "Running";
4 }
5}
6
7public class MockService extends Service {
8
9 // Compile error — cannot reduce visibility from public to protected
10 @Override
11 protected String getStatus() {
12 return "Mock";
13 }
14}Compile error: getStatus() in MockService cannot override getStatus() in Service; attempting to assign weaker access privileges; was public
An overriding method can widen access (from protected to public) but never narrow it. The parent declared public — every caller holding a Service reference expects to call getStatus() publicly, and the override cannot break that expectation.
Mistake 4 — Assuming Static Method Override Is Polymorphic
1public class Parent {
2 public static String type() { return "Parent"; }
3 public String name() { return "Parent"; }
4}
5
6public class Child extends Parent {
7 public static String type() { return "Child"; } // hiding, not overriding
8 @Override
9 public String name() { return "Child"; } // true override
10}
11
12public class StaticMistakeDemo {
13 public static void main(String[] args) {
14 Parent obj = new Child();
15 System.out.println(obj.type()); // resolved by reference type: Parent
16 System.out.println(obj.name()); // resolved by object type: Child
17 }
18}Output:
Parent
Child
type() is static — the compiler resolves it using the reference type (Parent), so "Parent" prints even though the object is a Child. name() is an instance method — the JVM resolves it using the actual object type (Child), so "Child" prints. Static methods are hidden, not overridden, and hiding is never polymorphic.
Interview Questions
Q1. What is the difference between method overloading and method overriding in Java?
Overloading defines multiple methods with the same name but different parameter lists within the same class. The compiler resolves which version to call at compile time — it is static polymorphism. Overriding defines a method in a subclass with the same name and parameter list as a parent method, replacing the parent's implementation. The JVM resolves which version to call at runtime based on the actual object type — it is dynamic polymorphism. They differ in when resolution happens, whether inheritance is needed, and what must match in the signature.
Q2. Can you override a static method in Java?
No. Static methods belong to the class, not to instances, so they cannot participate in runtime polymorphism. When a subclass declares a static method with the same signature as a parent's static method, it is called method hiding — the parent's version still exists and is called when accessed through a parent-type reference. Hiding is resolved at compile time by reference type, so it is never polymorphic. Putting @Override on a static method in a subclass causes a compile error in Java.
Q3. What happens if an overriding method throws a new checked exception not declared in the parent?
It causes a compile error. An overriding method can throw the same checked exceptions declared in the parent, a subset of them, or unchecked exceptions — but it cannot add new checked exceptions that the parent did not declare. This rule protects callers who hold a parent-type reference: they handle only the exceptions the parent declares, so the override cannot surprise them with undeclared ones.
Q4. Can you overload the main method in Java?
Yes. The JVM looks for the specific signature public static void main(String[] args) as the program entry point. You can define additional main methods with different parameter lists — main(int[] args), main(String arg) — and they compile without error. The JVM will not call them automatically at startup, but your own code can call them like any other method.
Q5. What is covariant return type in method overriding?
Covariant return type means an overriding method can return a more specific (subtype) type than the parent method. If the parent returns Animal, the override can return Dog — since Dog is a subtype of Animal. This was introduced in Java 5 and is most commonly seen in factory methods and builder hierarchies where the subclass's factory method returns the specific subclass type, avoiding unnecessary casting for the caller.
Q6. How does the compiler choose between overloaded methods when arguments could match multiple signatures?
The compiler applies a ranking: it first tries exact type match, then widening conversion (int can widen to long, float, double), then autoboxing, then varargs as a last resort. If two signatures match with equal priority and neither is more specific, the compiler reports an ambiguous method call error. This ordering explains why print(int) is preferred over print(long) when you pass an int, and why print(Integer) (autoboxed) is preferred over print(int...) (varargs).
FAQs
Can overloading happen across parent and child classes?
Yes. If a parent class defines display(String) and a child class defines display(int), the child class has both versions available — one inherited, one its own. This is a valid overload that works across the inheritance hierarchy. It is different from overriding because the parameter lists differ and the parent's method is not replaced.
Is method overloading possible with just a different return type?
No. The compiler selects overloaded methods based on argument types and count, not the return type. Two methods with the same name and parameter list but different return types cause a compile error — the compiler considers them duplicates regardless of what they return.
What does @Override do exactly — is it mandatory?
@Override is an annotation that tells the compiler to verify this method genuinely overrides something in a superclass or implements something from an interface. It is not required for overriding to work, but it is treated as mandatory in every serious Java codebase. Without it, a typo in the method name silently creates a new unrelated method instead of an override — a bug that is extremely hard to spot at runtime.
Can a constructor be overloaded? Can it be overridden?
Constructors can be overloaded — having multiple constructors in the same class with different parameter lists is one of the most common patterns in Java. Constructors cannot be overridden — they are not inherited, and overriding requires inheritance. Subclasses call parent constructors via super() but never override them.
What is the difference between method hiding and method overriding?
Method overriding applies to instance methods and is resolved polymorphically at runtime by the JVM based on the actual object type. Method hiding applies to static methods — when a subclass declares a static method with the same signature as a parent's static method, the parent version is hidden but not replaced. Hidden static methods are resolved at compile time by reference type and are never polymorphic. The practical consequence: an object of the child type held in a parent reference will call the parent's static method — not the child's.
Summary
Overloading and overriding are both about methods sharing a name, but the similarity ends there. Overloading gives callers flexibility — multiple signatures for the same concept, resolved before the program runs. Overriding gives subclasses identity — their own behaviour for an inherited contract, resolved when the program is actually executing.
In production code, you use overloading to handle varying input shapes without forcing callers to remember different method names. You use overriding to implement polymorphic behaviour — the core mechanism that makes List, PaymentProcessor, and every other abstraction work without caring about what sits underneath. Getting either wrong produces subtle bugs that compile cleanly and break unpredictably at runtime.
Both appear in almost every Java interview. Interviewers asking about overloading want to see that you understand compile-time resolution and signature rules. Interviewers asking about overriding want to see that you understand the JVM's runtime dispatch, the @Override annotation, and what happens with static methods. The comparison table in this article covers every dimension they are likely to probe.
What to Read Next
| Topic | Link |
|---|---|
| How polymorphism uses method overriding for runtime dispatch in class hierarchies | Java Polymorphism → |
| How inheritance provides the class hierarchy that overriding builds on | Java Inheritance → |
| How abstract methods force subclasses to provide an override implementation | Java Abstract Classes → |
| How interfaces define contracts that implementing classes must override | Java Interfaces → |
| How constructors can be overloaded to provide flexible object creation | Java Constructors → |