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.
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
| Overloading | Overriding | |
|---|---|---|
| Decided | Compile time | Runtime |
| Decided by | Compiler, from the arguments | JVM, from the object |
| Lives in | One class | Parent and child |
| Parameters | Must differ | Must match |
| Return type | Can differ | Same, or a subtype |
static / private / final | Fine to overload | Cannot override |
| Access modifier | Whatever you want | Can'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.