Method Overloading vs Overriding
Introduction
In the previous tutorial on polymorphism, we introduced two mechanisms: method overloading (compile-time) and method overriding (runtime). These are among the most commonly confused concepts in OOP interviews and daily coding.
Both involve methods with the same name, but that's where the similarity ends. They solve different problems, work at different levels, and follow different rules.
This tutorial gives you a definitive side-by-side comparison with concrete examples, edge cases, and the rules you need to know. By the end, you'll never confuse them again.
The Core Difference
| Aspect | Method Overloading | Method Overriding |
|---|---|---|
| What varies | Parameters (number, type, or order) | Implementation (body of the method) |
| Where | Within the same class (or inherited) | Between parent and child classes |
| Resolved | At compile time (static binding) | At runtime (dynamic binding/dispatch) |
| Purpose | Provide multiple ways to call a method | Let subclasses customize inherited behavior |
| Return type | Can differ (with restrictions in Java) | Must be same (or covariant in Java) |
| Method signature | Must differ (different params) | Must be identical (same params) |
| Polymorphism type | Compile-time (static) | Runtime (dynamic) |
| Python support | ❌ Not directly (use default params) | ✅ Fully supported |
| Java support | ✅ Fully supported | ✅ Fully supported |
The simplest way to remember: Overloading = same name, different parameters. Overriding = same name, same parameters, different class.

Method Overloading — Same Name, Different Signatures
Method overloading lets you define multiple methods with the same name but different parameter lists within the same class. The compiler picks the right version based on the arguments.
Rules for overloading (Java):
- Method name must be the same
- Parameter list must differ (number, type, or order of parameters)
- Return type alone is NOT enough to distinguish overloaded methods
- Access modifiers can differ
- Can throw different exceptions
public class Logger {
// Overloaded log() — same name, different parameters
// Version 1: simple message
public void log(String message) {
System.out.println("[INFO] " + message);
}
// Version 2: message with severity level
public void log(String level, String message) {
System.out.printf("[%s] %s%n", level.toUpperCase(), message);
}
// Version 3: message with severity and context
public void log(String level, String message, String context) {
System.out.printf("[%s] [%s] %s%n", level.toUpperCase(), context, message);
}
// Version 4: message with numeric error code
public void log(int errorCode, String message) {
System.out.printf("[ERROR-%d] %s%n", errorCode, message);
}
// ❌ INVALID: Can't overload by return type alone!
// public int log(String message) { ... } // COMPILE ERROR
public static void main(String[] args) {
Logger logger = new Logger();
logger.log("Server started"); // Version 1
logger.log("WARN", "Memory usage high"); // Version 2
logger.log("ERROR", "Connection failed", "Database"); // Version 3
logger.log(500, "Internal Server Error"); // Version 4
// Output:
// [INFO] Server started
// [WARN] Memory usage high
// [ERROR] [Database] Connection failed
// [ERROR-500] Internal Server Error
}
}# Python doesn't support method overloading directly.
# If you define two methods with the same name, the second one replaces the first.
# Instead, Python uses DEFAULT PARAMETERS and *args/**kwargs:
class Logger:
"""Python approach to achieve overloading behavior."""
def log(self, message: str, level: str = "INFO",
context: str | None = None) -> None:
"""Single method with optional parameters replaces 3 overloaded methods."""
if context:
print(f"[{level.upper()}] [{context}] {message}")
else:
print(f"[{level.upper()}] {message}")
def log_error(self, error_code: int, message: str) -> None:
"""Separate method name when parameters are fundamentally different."""
print(f"[ERROR-{error_code}] {message}")
logger = Logger()
logger.log("Server started") # defaults: INFO, no context
logger.log("Memory usage high", level="WARN") # custom level
logger.log("Connection failed", level="ERROR", context="Database") # full detail
logger.log_error(500, "Internal Server Error") # different method
# Output:
# [INFO] Server started
# [WARN] Memory usage high
# [ERROR] [Database] Connection failed
# [ERROR-500] Internal Server Error
# ALTERNATIVE: Python 3.10+ @overload (type hints only — no runtime behavior)
from typing import overload
class TypedLogger:
@overload
def log(self, message: str) -> None: ...
@overload
def log(self, level: str, message: str) -> None: ...
def log(self, *args) -> None:
"""Actual implementation handles both signatures."""
if len(args) == 1:
print(f"[INFO] {args[0]}")
elif len(args) == 2:
print(f"[{args[0].upper()}] {args[1]}")Method Overriding — Same Signature, Different Class
Method overriding lets a child class provide its own implementation for a method inherited from the parent. The method signature (name + parameters) must be identical.
Rules for overriding:
- Method name and parameter list must be identical to the parent's
- Return type must be the same (or a subclass in Java — called covariant return)
- Access modifier cannot be more restrictive (e.g., can't override a
publicmethod asprivate) - Java: Use
@Overrideannotation to catch mistakes at compile time - Java:
finalmethods cannot be overridden - Java:
staticmethods cannot be overridden (they're hidden, not overridden)
class PaymentProcessor:
"""Base class — defines the default payment flow."""
def process(self, amount: float) -> bool:
"""Process a payment. Subclasses override this."""
print(f"Processing ${amount:.2f} with generic processor")
return True
def get_fee(self, amount: float) -> float:
"""Calculate processing fee. Subclasses override this."""
return amount * 0.03 # Default 3%
def receipt(self, amount: float) -> str:
"""Generate a receipt. Subclasses can extend this using super()."""
fee = self.get_fee(amount)
return f"Amount: ${amount:.2f}, Fee: ${fee:.2f}, Total: ${amount + fee:.2f}"
class StripeProcessor(PaymentProcessor):
"""Overrides payment methods for Stripe-specific behavior."""
def process(self, amount: float) -> bool:
"""Override: Stripe-specific processing."""
print(f"Stripe: charging ${amount:.2f} via Stripe API")
# In real code: stripe.Charge.create(...)
return True
def get_fee(self, amount: float) -> float:
"""Override: Stripe charges 2.9% + $0.30."""
return amount * 0.029 + 0.30
class PayPalProcessor(PaymentProcessor):
"""Overrides payment methods for PayPal-specific behavior."""
def process(self, amount: float) -> bool:
"""Override: PayPal-specific processing."""
print(f"PayPal: charging ${amount:.2f} via PayPal REST API")
return True
def get_fee(self, amount: float) -> float:
"""Override: PayPal charges 3.49% + $0.49."""
return amount * 0.0349 + 0.49
def receipt(self, amount: float) -> str:
"""Extend: call parent's receipt and add PayPal transaction ID."""
base_receipt = super().receipt(amount) # Reuse parent logic
return base_receipt + f"\nPayPal Ref: PP-{id(self):08X}"
# --- Polymorphism via overriding ---
def checkout(processor: PaymentProcessor, amount: float) -> None:
"""Works with ANY processor. Doesn't know the specific type."""
processor.process(amount)
print(processor.receipt(amount))
print()
checkout(StripeProcessor(), 100.0)
checkout(PayPalProcessor(), 100.0)
# Output:
# Stripe: charging $100.00 via Stripe API
# Amount: $100.00, Fee: $3.20, Total: $103.20
#
# PayPal: charging $100.00 via PayPal REST API
# Amount: $100.00, Fee: $3.99, Total: $103.99
# PayPal Ref: PP-7F2B3A40public class PaymentProcessor {
public boolean process(double amount) {
System.out.printf("Processing $%.2f with generic processor%n", amount);
return true;
}
public double getFee(double amount) {
return amount * 0.03; // Default 3%
}
public String receipt(double amount) {
double fee = getFee(amount);
return String.format("Amount: $%.2f, Fee: $%.2f, Total: $%.2f",
amount, fee, amount + fee);
}
}
class StripeProcessor extends PaymentProcessor {
@Override // Always use @Override to catch signature mismatches!
public boolean process(double amount) {
System.out.printf("Stripe: charging $%.2f via Stripe API%n", amount);
return true;
}
@Override
public double getFee(double amount) {
return amount * 0.029 + 0.30; // Stripe: 2.9% + $0.30
}
}
class PayPalProcessor extends PaymentProcessor {
@Override
public boolean process(double amount) {
System.out.printf("PayPal: charging $%.2f via PayPal REST API%n", amount);
return true;
}
@Override
public double getFee(double amount) {
return amount * 0.0349 + 0.49; // PayPal: 3.49% + $0.49
}
@Override
public String receipt(double amount) {
String baseReceipt = super.receipt(amount); // Extend parent logic
return baseReceipt + "\nPayPal Ref: PP-" +
String.format("%08X", System.identityHashCode(this));
}
}
class OverridingDemo {
static void checkout(PaymentProcessor processor, double amount) {
processor.process(amount);
System.out.println(processor.receipt(amount));
System.out.println();
}
public static void main(String[] args) {
checkout(new StripeProcessor(), 100.0);
checkout(new PayPalProcessor(), 100.0);
}
}Visualization

Edge Cases and Tricky Scenarios
Scenario 1: Can You Overload and Override at the Same Time?
Yes! A child class can override a parent method AND also overload it with a new parameter list:
class Parent {
public void greet(String name) {
System.out.println("Hello, " + name);
}
}
class Child extends Parent {
@Override
public void greet(String name) { // Override: same signature
System.out.println("Hi, " + name + "!");
}
public void greet(String name, int times) { // Overload: different params
for (int i = 0; i < times; i++) greet(name);
}
}
Scenario 2: Changing Only the Return Type
In Java, you cannot overload methods by changing only the return type — the compiler can't distinguish int add(int, int) from double add(int, int) because the call site add(3, 5) is identical.
For overriding, Java allows covariant return types — the child can return a more specific type than the parent:
class Factory {
public Animal create() { return new Animal(); }
}
class DogFactory extends Factory {
@Override
public Dog create() { return new Dog(); } // Dog IS-A Animal → allowed
}
Scenario 3: Static Methods
Static methods belong to the class, not to instances. In Java, static methods cannot be overridden — they're hidden instead. The method called depends on the declared type, not the actual type. This is a common interview trick question.
Common Mistakes
Mistake 1: Thinking Overloading Is Overriding
❌ Wrong:
class Child extends Parent {
// This is OVERLOADING (different params), not OVERRIDING!
public void process(String data, int flags) { ... }
}
If the parent has process(String data), this adds a new method — it doesn't replace the parent's.
✅ Right: To override, the signature must be identical: public void process(String data).
Mistake 2: Forgetting @Override in Java
Without @Override, a typo in the method name creates a new method instead of overriding:
// Intended to override calculatePay() but has a typo:
public double caluclatePay() { ... } // New method, not an override!
@Override catches this at compile time.
Mistake 3: Over-Overloading
Having 8+ overloaded variants of the same method makes the API confusing. Consider using the Builder Pattern (Section 9) or a parameter object instead.
Key Takeaways
- Overloading: same method name, different parameters, within the same class. Resolved at compile time. A convenience feature
- Overriding: same method name AND parameters, in a child class replacing the parent's version. Resolved at runtime. A design tool
- Python doesn't support traditional overloading — use default parameters,
*args/**kwargs, or@singledispatchinstead - In Java, always use
@Overridewhen overriding — it catches signature mismatches at compile time - You can override AND overload in the same child class
- Static methods cannot be overridden in Java (they're hidden, resolved by declared type)
- Return type alone cannot distinguish overloaded methods. Overridden methods must have the same (or covariant) return type
- Runtime polymorphism (overriding) is the foundation of extensible design patterns — Strategy, Template Method, Factory, and more
- In the next tutorial, we'll cover enums — a way to define type-safe constants that leverage OOP principles