Skip to main content

Method Overloading vs Overriding

Editorial

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

AspectMethod OverloadingMethod Overriding
What variesParameters (number, type, or order)Implementation (body of the method)
WhereWithin the same class (or inherited)Between parent and child classes
ResolvedAt compile time (static binding)At runtime (dynamic binding/dispatch)
PurposeProvide multiple ways to call a methodLet subclasses customize inherited behavior
Return typeCan differ (with restrictions in Java)Must be same (or covariant in Java)
Method signatureMust differ (different params)Must be identical (same params)
Polymorphism typeCompile-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.

Overloading vs overriding. Overloading: same name, different parameter lists, one class; compiler picks by argument types at compile time (Logger.log(String), log(String,String), log(int,String)) - a convenience. Overriding: same name AND parameters, a child replaces the parent's version; runtime picks by the actual object type - dynamic dispatch (PaymentProcessor.process -> Stripe.process / PayPal.process, identical signature) - the design tool. Comparison: what varies (parameters vs implementation), where (same class vs parent->child), resolved (compile time vs runtime), signature (must differ vs must be identical), picked by (argument types vs the actual object). Mnemonic: overloading = same name different parameters (compile-time); overriding = same name same parameters different class (runtime).

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):

  1. Method name must be the same
  2. Parameter list must differ (number, type, or order of parameters)
  3. Return type alone is NOT enough to distinguish overloaded methods
  4. Access modifiers can differ
  5. 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:

  1. Method name and parameter list must be identical to the parent's
  2. Return type must be the same (or a subclass in Java — called covariant return)
  3. Access modifier cannot be more restrictive (e.g., can't override a public method as private)
  4. Java: Use @Override annotation to catch mistakes at compile time
  5. Java: final methods cannot be overridden
  6. Java: static methods 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-7F2B3A40
public 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

Two resolvers side by side. Left (overloading): pick a log(...) call and the compiler matches the overload by argument types (same class, compile-time). Right (overriding): pick the object (Generic/Stripe/PayPal) then call an identical-signature method and the runtime dispatches to that object's override, inherited, or extended version.

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 @singledispatch instead
  • In Java, always use @Override when 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