All posts
4 min read

Method overloading vs overriding in Java, explained properly

Both let two methods share a name. One gets decided while your code compiles, the other while it runs, and almost every interview question about them is really checking whether you know which.

JavaOOPFundamentals

Ask ten students the difference and you get the same sentence back. "Overloading is same name different parameters, overriding is same name same parameters."

Correct. Also useless. It falls apart the second anyone asks a follow-up, which is exactly why interviewers keep asking.

The thing that actually separates them is when Java decides which method to run. Overloading gets settled while your code is compiling. Overriding gets settled while your program is running. Every other rule you've memorised about them comes out of that one difference.

Overloading: the compiler already knows

Same class, same method name, different parameter lists.

class Invoice {
    double total(double price) {
        return price;
    }

    double total(double price, double taxRate) {
        return price + (price * taxRate);
    }

    double total(double price, double taxRate, double discount) {
        return (price - discount) + ((price - discount) * taxRate);
    }
}

You write invoice.total(500, 0.18). The compiler counts two doubles, picks the second method, and burns that choice into the bytecode. Done. Your program hasn't started yet and the decision is already made.

That's why people call it compile-time polymorphism. Nothing is being figured out at runtime because there's nothing left to figure out.

It also explains a rule everyone memorises and nobody questions:

double total(double price) { ... }
int    total(double price) { ... }   // won't compile

Why not? Because the compiler chooses based on the call site, and at the call site total(500) looks identical either way. Return type gives it nothing to work with. So it just refuses.

Overriding: the object decides

A subclass replaces a method it inherited.

class Payment {
    void process() {
        System.out.println("Processing a generic payment");
    }
}

class UpiPayment extends Payment {
    @Override
    void process() {
        System.out.println("Opening UPI app, waiting for PIN");
    }
}

class CardPayment extends Payment {
    @Override
    void process() {
        System.out.println("Charging card, running 3D Secure");
    }
}

Here's where it gets interesting:

Payment payment = new UpiPayment();
payment.process();   // "Opening UPI app, waiting for PIN"

Look at the variable. Its type is Payment. The compiler has no idea which subclass is actually in there, and it doesn't try to guess. It punts.

Then at runtime the JVM looks at the object sitting in memory, sees a UpiPayment, and calls that one.

This is the whole reason "program to an interface" works. You write payment.process() once. Six months later you add NetBankingPayment and that line keeps working, untouched.

Side by side

OverloadingOverriding
DecidedCompile timeRuntime
Decided byCompiler, from the argumentsJVM, from the object
Lives inOne classParent and child
ParametersMust differMust match
Return typeCan differSame, or a subtype
static / private / finalFine to overloadCannot override
Access modifierWhatever you wantCan't be more restrictive

The one that catches everybody

This shows up constantly and most people answer it wrong.

class Parent {
    static void greet() { System.out.println("Parent"); }
}

class Child extends Parent {
    static void greet() { System.out.println("Child"); }
}

Parent p = new Child();
p.greet();

Nearly everyone says "Child". It prints Parent.

static methods belong to the class, not to any object. So there's no object to look at, and the JVM never gets involved at all. The compiler resolves it from the declared type of p, which is Parent, and that's the end of it.

What's happening here isn't overriding. It's hiding, and it behaves nothing like overriding despite looking identical on the page.

Try adding @Override to Child.greet(). The compiler rejects it. Which is honestly the best argument for writing that annotation every single time: it turns a misunderstanding you'd never notice into a build error you can't ignore.

Why you should care past the interview

Overloading is convenience. It keeps you from naming things totalWithTax, totalWithDiscount, totalWithTaxAndDiscount.

Overriding is structure. It's what lets you write code against Payment and extend it later without touching what already works. Spring, Android, basically every framework you'll use is built on it.

If you keep one sentence from this: overloading is a question the compiler can answer, overriding is a question only a running program can. Everything in that table above is just a consequence of that.


I write about Java, Flutter, and whatever I've recently broken. Couple of times a month, only when there's something worth saying. Subscribe here if that sounds useful.

ShareXLinkedInWhatsApp

Written by

Zaid Sayyed

Software Engineer · AI & Cross-Platform Apps. Currently open to roles and freelance work.