Java Tutorial
🔍

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.

Java
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}
Java
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.

Java
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}
Java
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}
Java
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}
Java
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

AspectMethod OverloadingMethod Overriding
DefinitionSame method name, different parameter list in the same classSame method name and signature in a subclass replacing the parent's version
Polymorphism typeCompile-time (static) polymorphismRuntime (dynamic) polymorphism
Resolved atCompile time — by the compilerRuntime — by the JVM
Class relationshipWorks within a single class (also works across parent-child)Requires inheritance — parent and child classes
Parameter listMust differ — type, count, or orderMust be identical
Return typeCan differ (does not alone distinguish overloads)Must be same or a subtype (covariant return)
Access modifierCan be anythingCannot reduce visibility from parent
@Override annotationNot applicableStrongly recommended — enforced by most teams
static methodsCan be overloadedCannot be overridden — only hidden
private methodsCan be overloadedCannot be overridden — not inherited
final methodsCan be overloadedCannot be overridden
Exception handlingCan throw any exceptionsCannot add new checked exceptions not declared in parent
When to useMultiple ways to call the same logic with different inputsSpecialised behaviour in a subclass for an inherited method
Exampleprint(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

RuleOverloadingOverriding
Same method nameRequiredRequired
Same parameter listProhibited (at least one must differ)Required (must match exactly)
Same return typeNot requiredRequired (or covariant subtype)
Inheritance neededNoYes
Works on static methodsYesNo — static methods are hidden, not overridden
Works on private methodsYesNo — private methods are not inherited
Works on final methodsYesNo — final prevents overriding
Checked exception rulesNo restrictionsCannot declare new checked exceptions
AnnotationNone 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.

Java
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}
Java
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}
Java
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}
Java
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}
Java
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

Java
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

Java
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

Java
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

Java
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

TopicLink
How polymorphism uses method overriding for runtime dispatch in class hierarchiesJava Polymorphism →
How inheritance provides the class hierarchy that overriding builds onJava Inheritance →
How abstract methods force subclasses to provide an override implementationJava Abstract Classes →
How interfaces define contracts that implementing classes must overrideJava Interfaces →
How constructors can be overloaded to provide flexible object creationJava Constructors →
Java Method Overloading vs Overriding | DevStackFlow