Method overloading vs overriding in Java, explained properly
Both let two methods share a name. One is decided while your code compiles, the other while it runs — and almost every interview question about them is really testing whether you know which.
Every Java interview asks this, and most answers are some version of "overloading is same name different parameters, overriding is same name same parameters." That is true and it explains nothing. It also won't survive the follow-up question.
The real difference is when Java decides which method to call. Overloading is decided while your code is compiling. Overriding is decided while your program is running. Once that clicks, everything else about the two follows from it.
Overloading — the compiler picks
Overloading is one class having several methods with the same name and 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);
}
}
Three methods, one name. When you write invoice.total(500, 0.18), the compiler
looks at what you passed — two doubles — and locks in the second method. That
decision is baked into the bytecode before your program ever runs.
This is why it's called compile-time polymorphism. The compiler already knows the answer.
It also explains a rule people memorise without understanding:
double total(double price) { ... }
int total(double price) { ... } // won't compile
Return type alone can't distinguish them, because the compiler chooses from the
call site — and at the call site, total(500) looks identical either way. It
has no information to pick with, so it refuses.
Overriding — the object picks
Overriding is a subclass replacing 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");
}
}
Now the interesting part:
Payment payment = new UpiPayment();
payment.process(); // "Opening UPI app, waiting for PIN"
The variable's type is Payment. The compiler only knows it's some kind of
Payment — it cannot know which. So it doesn't decide. It defers.
At runtime, the JVM looks at the object actually sitting in memory, sees a
UpiPayment, and calls that version. This is runtime polymorphism, and it's
the mechanism the whole idea of programming to an interface rests on.
Write one line — payment.process() — and add a NetBankingPayment class six
months later without touching it.
Side by side
| Overloading | Overriding | |
|---|---|---|
| Decided | Compile time | Runtime |
| Decided by | The compiler, from arguments | The JVM, from the object |
| Where | One class | Parent and child |
| Parameters | Must differ | Must match exactly |
| Return type | Can differ (if params do) | Same, or a subtype |
private / static / final | Can be overloaded | Cannot be overridden |
| Access modifier | Anything | Cannot be more restrictive |
The follow-up that catches people
This one comes up constantly, and it's the reason understanding the timing matters more than memorising the table.
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(); // prints "Parent"
Most people say "Child". It prints Parent.
static methods belong to the class, not to any object. There's no object to
inspect at runtime, so there's nothing to dispatch on — the JVM never gets
involved. The compiler resolves it from the variable's declared type, Parent,
and that's final.
This isn't overriding at all. It's hiding, and it behaves completely
differently. Add @Override to Child.greet() and the compiler will reject it —
which is the single best reason to always write that annotation. It turns a silent
misunderstanding into a build error.
Why this actually matters
Overloading is convenience. It saves you from totalWithTax, totalWithDiscount,
totalWithTaxAndDiscount cluttering a class.
Overriding is architecture. It's what lets you write code against Payment and
extend it later without editing what already works. Every framework you use runs
on this — Spring, Android, all of it.
If you only remember one thing: overloading is a question the compiler can answer, overriding is a question only the running program can. Every rule in that table is a consequence of that one sentence.
Takeaways
- Overloading resolves at compile time from the arguments; overriding resolves at runtime from the object
- Return type alone can't overload — the call site gives the compiler nothing to work with
staticmethods are hidden, not overridden, and dispatch on the declared type- Always write
@Override; it converts a silent bug into a compile error
I write about Java, Flutter, and the things I get wrong while building — roughly a couple of times a month, and only when there's something worth saying. Subscribe here if that's useful to you.