What is the difference between overloading and overriding?
Overloading is resolved by the compiler from the declared argument types; overriding is resolved by the JVM from the object's runtime type. That one sentence predicts every surprising case — an Object variable holding a String picks the Object overload, a static method is hidden rather than overridden, and fields are not polymorphic at all.
The Answer
- Overloading is several methods with the same name and different parameters. The compiler picks one, from the declared types at the call site.
- Overriding is a subclass replacing an inherited method with the same signature. The JVM picks one, from the runtime type of the object.
- So overloading is decided once, at compile time, and baked into the bytecode. Overriding is decided on every call.
- That single difference explains the traps. An
Objectvariable holding aStringselects theObjectoverload, because the variable's type is what the compiler saw. - A static method is hidden, not overridden. It follows the reference type, so
Parent ref = new Child(); ref.tag()callsParent's. - Fields are never polymorphic either — same rule.
ref.labelgives youParent's field even though the object is aChild.
Understand It
One rule, and it predicts everything else
Say it precisely and the rest is bookkeeping: overloading looks at the reference, overriding looks at the object.
Object held = "I am really a String";
System.out.println("describe(held) : " + describe(held));
System.out.println("describe((String) held) : " + describe((String) held));
System.out.println("describe(null) : " + describe(null));describe(held) : describe(Object)
describe((String) held) : describe(String)
describe(null) : describe(String)The object on line one is a String at runtime. It does not matter. The variable is declared Object, the compiler had only that to work with, and it wrote describe(Object) into the bytecode permanently. The cast on line two does not convert anything at runtime — it changes what the compiler sees, which is the only thing that was ever in question.
Line three is the other half of the rule: when several overloads apply, the compiler picks the most specific one. null is assignable to both Object and String, and String is more specific, so it wins. Add a third overload taking Integer and the call stops compiling altogether — String and Integer are equally specific and neither is more so:
error: reference to describe is ambiguous
both method describe(String) and method describe(Integer) match
The three phases, and why widening beats boxing
When you call pick(7), the compiler does not consider every candidate at once. It runs three passes and stops at the first that finds a match:
- Without boxing or varargs — only widening primitive conversions.
- With boxing, still no varargs.
- With varargs.
System.out.println("pick(7) : " + pick(7));
System.out.println("pick(7L) : " + pick(7L));
System.out.println("pick(7,8) : " + pick(7, 8));pick(7) : pick(long) — widening
pick(7L) : pick(long) — widening
pick(7,8) : pick(int...) — varargspick(7) has three plausible answers — widen to long, box to Integer, or wrap in an int[]. Widening wins because phase one succeeded and the later phases were never reached.
The reason for the ordering is backward compatibility: autoboxing and varargs both arrived in Java 5, and if they had competed on equal terms, adding an overload could have silently changed which method existing code called. Phasing them behind widening means Java 1.4 code keeps resolving exactly as it did.
Practical consequence, and the reason this is worth knowing: overloading on types that are convertible to each other is a design smell. remove(int) and remove(Object) on List is the JDK's own famous example, and it still catches people — see integer-caching.
Overriding is the opposite, and static methods are the tell
Parent ref = new Child();
System.out.println("ref.name() : " + ref.name());
System.out.println("ref.tag() : " + ref.tag());
System.out.println("ref.label : " + ref.label);
System.out.println("((Child) ref).label : " + ((Child) ref).label);ref.name() : Child.name() — instance
ref.tag() : Parent.tag() — static
ref.label : Parent.label
((Child) ref).label : Child.labelFour lines, one object, and three of them answer "Parent". Only name() — the instance method — consults the object.
tag() is the answer to "can you override a static method?" You cannot. Child.tag() hides Parent.tag() rather than overriding it, and a hidden method is resolved from the reference type at compile time. Note also that ref.tag() compiles at all: calling a static through an instance reference is legal and is exactly why this is confusing. Most IDEs warn; write Parent.tag() and the confusion disappears.
Fields follow the same rule as statics, which is the part people have usually never seen. Child.label shadows Parent.label; both exist in the object simultaneously, and which one you get depends on the type of the expression you read it through. This is why "make the field protected and override it in the subclass" is not a thing — you cannot override a field, only shadow it, and shadowing is almost always a bug.
At the bytecode level it is one instruction's difference: invokevirtual consults the object's method table, invokestatic and getfield do not.
What makes something an override at all
The signature must match, and "match" has rules the compiler will check for you if you ask it to:
- Same name and same parameter types. Different parameters is an overload, not an override, and that is the single most common accidental bug in this area.
- Return type identical, or a subtype Java 5 (1.5)+since covariant returns arrived in 5.
- Access may widen, never narrow.
protectedcan becomepublic;publiccannot becomeprotected. - Checked exceptions may narrow, never widen. Unchecked exceptions are unrestricted.
private,staticandfinalmethods cannot be overridden.privateis not even visible;staticis hidden;finalis refused.
@Override does not change behaviour — it asks the compiler to verify that a method really does override something. Which is why it matters here more than anywhere else:
class Base { public boolean equals(Object o) { return true; } }
class Broken extends Base {
@Override
public boolean equals(Broken other) { return true; } // ERROR, thanks to @Override
}
error: method does not override or implement a method from a supertype
Without @Override that compiles happily as an overload, and Broken silently keeps Object.equals — which is the classic way a HashSet full of equal-looking objects ends up with duplicates. See hashcode-equals-contract. Put @Override on every override; it costs nothing and it catches this.
Reference
The comparison
| Overloading | Overriding | |
|---|---|---|
| Also called | static / compile-time binding | dynamic / runtime binding |
| Decided by | the compiler | the JVM, per call |
| Decided from | the declared types at the call site | the runtime type of the receiver |
| Requires inheritance | no | yes |
| Parameters | must differ | must be identical |
| Return type | may differ freely | identical or a subtype |
| Access modifier | unrestricted | may widen, never narrow |
| Checked exceptions | unrestricted | may narrow, never widen |
Applies to static | yes | no — that is hiding |
| Applies to fields | n/a | no — that is shadowing |
| Bytecode | baked into the call site | invokevirtual / invokeinterface |
The rules for a legal override, as code
class Base {
protected Number compute(String input) throws IOException { return 1; }
}
class Derived extends Base {
@Override
public Integer compute(String input) throws FileNotFoundException { return 2; }
// ^ widened ^ covariant ^ narrower checked exception — all legal
}
// Each of these is rejected:
// Object compute(String s) return type is not a subtype of Number
// protected Number compute(Object s) different parameter — an overload, not an override
// private Number compute(String s) cannot reduce visibility
// Number compute(String s) throws Exception broader checked exception
Preventing an override
// final on the method — nobody may replace it.
public final void audit() { }
// final on the class — nobody may extend it at all.
public final class Money { }
// private — invisible to subclasses, so a same-named method is unrelated.
private void internal() { }
// sealed — a named list may extend, and each must say final, sealed or
// non-sealed. See /java/modern-java/sealed-classes
public sealed class Payment permits Card, Upi { }
The one to reach for by default is final on a class you did not design for extension. "Design and document for inheritance, or else prohibit it" is Effective Java's phrasing and it has not aged.
The overloads to avoid writing
// 1. Overloads whose parameters are convertible. The caller cannot see which
// one they hit, and adding one later silently redirects existing calls.
void log(int code) { }
void log(Integer code) { }
void log(long code) { }
// 2. Overloads with the same arity and unrelated types — null becomes
// ambiguous and the call will not compile at all.
void send(String to) { }
void send(Integer id) { }
// send(null) -> error: reference to send is ambiguous
// 3. An overload that does something DIFFERENT. Same name should mean the
// same operation; List.remove(int) vs remove(Object) is the JDK's own
// scar tissue.
// Prefer distinct names. sendByEmail / sendById is longer and never wrong.
Scenarios
A subclass "override" that never runs. Someone reports that their custom equals is ignored and duplicates are appearing in a HashSet. The method is equals(MyType other) — a different signature from equals(Object), so it is an overload, and the collection calls Object.equals via the interface. Nothing about the code looks wrong. @Override on every intended override turns this from a production bug into a compile error, which is the entire argument for the annotation.
Adding an overload changes behaviour in code you did not touch. A library adds process(Object) next to an existing process(String). Callers passing a String are unaffected; callers passing a variable declared Object that happens to hold a String were already hitting nothing and now compile against the new method. Worse, adding process(long) next to process(int) silently redirects every existing process(someInt) call, because widening is tried first. Adding an overload to a published API is a source-compatible change that can still change behaviour — treat it with the caution you would give a signature change.
A test passes because the mock's method is an overload. A hand-written stub declares save(Order order) while the interface declares save(Entity entity). The stub compiles, the test passes, and it verified nothing — production calls the interface method and gets the inherited default or an abstract failure. This is why @Override belongs on stub methods too, and a large part of why mocking frameworks are safer than hand-rolled doubles for interfaces you do not own.
Someone wants to "override" a constant in a subclass. They declare protected String prefix = "base" and redeclare it in the subclass, and half the code sees one value and half the other depending on the static type of the variable. Fields shadow; they never override. The fix is a protected String prefix() method the subclass overrides, or constructor injection. If you see the same field name declared in a class and its subclass, it is almost always a bug rather than a design.
Interviewer's Next Move
1. "Can you override a static method?"
No. A same-signature static in a subclass hides the parent's, and hiding is resolved from the reference type at compile time — so Parent ref = new Child(); ref.tag() calls Parent's. The reason is mechanical: overriding needs dynamic dispatch through the object's method table, and invokestatic never consults an object at all.
2. "Are fields polymorphic?" No, and they follow exactly the same rule as static methods. A field in a subclass with the same name shadows the parent's; both exist in the object, and which one you read depends on the declared type of the expression. It is why you cannot "override a field", and why seeing the same field name in a class and its subclass is nearly always a bug.
3. "Object o = "hello"; describe(o); with overloads for Object and String — which runs?"
describe(Object). Overload resolution is a compile-time decision made from the declared type of the argument, and at compile time all it knew was Object. Casting to String changes what the compiler sees and therefore changes the answer; the runtime type never enters into it.
4. "What happens with describe(null)?"
The most specific applicable overload wins, so describe(String) over describe(Object). If two candidates are equally specific — String and Integer, say — the call is ambiguous and does not compile. Casting the null, describe((String) null), is how you disambiguate.
5. "What can an override change about the signature?"
Parameters: nothing, they must be identical. Return type: may narrow to a subtype, since covariant returns in Java 5. Access: may widen, never narrow. Checked exceptions: may narrow or drop them, never add broader ones. And the method must not be private, static or final in the parent.
6. "Why does widening beat boxing?" Because overload resolution runs in three phases — widening, then boxing, then varargs — and stops at the first that finds a candidate. The ordering exists for compatibility: boxing and varargs both arrived in Java 5, and putting them behind widening guarantees that pre-5 code keeps binding to exactly the methods it always did.
Code traps
class Parent {
Parent() { System.out.println("ctor sees " + who()); }
String who() { return "parent"; }
}
class Child extends Parent {
private final String name = "child";
@Override String who() { return name; }
}
new Child();
Answer
ctor sees child, which is not the answer this trap usually has. The override does run — who() is dispatched on the object, which is a Child from the first instruction of construction — but the textbook version of this puzzle expects null, because a subclass field is not assigned until after super() returns.
The reason you get child here is that private final String name = "child" is a compile-time constant: final, of a constant type, with a constant-expression initialiser. javac folds the literal directly into who(), so no field is ever read.
Break any one of those three conditions and the usual answer comes back:
private final String name = "child"; -> ctor sees child
private final String name = String.valueOf("child") -> ctor sees null
private String name = "child"; -> ctor sees nullWhich is the real lesson: never call an overridable method from a constructor. The symptom does not merely depend on subclass state, it depends on whether the compiler was able to constant-fold the initialiser — so the bug can appear and disappear on an edit that looks cosmetic.
static void show(Integer i) { System.out.println("Integer"); }
static void show(int... i) { System.out.println("varargs"); }
static void show(long l) { System.out.println("long"); }
show(5);
Answer
long. Phase one allows only widening, and int widens to long, so resolution succeeds before boxing or varargs are ever considered. Delete the long overload and it prints Integer; delete that too and it prints varargs. The order is fixed and has nothing to do with declaration order in the file.
Common wrong answers
| Said in interviews | Reality |
|---|---|
| "Overloading is runtime polymorphism." | It is compile-time. Overriding is the runtime one. |
| "You can override a static method." | You hide it, and hiding follows the reference type. |
| "Changing the return type overloads a method." | It does not — same name and parameters with a different return type does not compile. |
"@Override makes the method override something." | It only checks that it does. Behaviour is identical without it; the value is the compile error. |
| "The runtime type decides which overload runs." | The declared type does, always. |
"Fields can be overridden if they are protected." | Fields shadow, never override. Access has nothing to do with it. |
| "An override can throw whatever it likes." | Checked exceptions may only narrow. Unchecked ones are unrestricted. |
Check Yourself
Q1. In one sentence, what decides an overload and what decides an override?
Answer
The compiler decides an overload from the declared types at the call site; the JVM decides an override from the runtime type of the object. Every surprising case in this topic follows from that one difference.
Q2. Parent ref = new Child(); — ref.name() gives Child's and ref.tag() gives Parent's. Why the difference?
Answer
name() is an instance method, so it is overridden and dispatched through the object's method table by invokevirtual. tag() is static, so Child.tag() merely hides Parent.tag(), and invokestatic resolves it from the reference type at compile time — no object is consulted.
Q3. Your equals is being ignored and duplicates appear in a HashSet. What is the likely cause and the one-line preventative?
Answer
You wrote equals(MyType) instead of equals(Object), so it is an overload and the collection calls the inherited Object.equals. The preventative is @Override on every intended override — it turns this from a silent production bug into a compile error.
Practice
| Tier | Exercise | Time |
|---|---|---|
| Warm-up | Predict eight lines | 5 min |
| Challenge | The equals that was ignored | 20 min |
| Production | The notifier that stopped notifying | 45 min |
| Interview | Full round replay — overloading and overriding | 10 min |
What changed, and when
Version boundaries are where follow-ups live. Know the change and the behaviour it replaced.
- Java 5 (1.5)
Autoboxing and varargs join overload resolution as phases two and three, so a call that used to have one candidate can now have several — and widening still wins over both.
Before Java 5 (1.5): Only widening applied, so int never matched an Integer parameter and there was no varargs phase. Resolution had fewer ways to surprise you.
- Java 5 (1.5)
Covariant return types arrive: an override may return a subtype of what it overrides, which is what lets clone() and builder methods return the precise type.
Before Java 5 (1.5): An override had to return exactly the same type, so every caller of a subclass method still had to cast the result.
Practice ladder
Reading this page is not knowing it. Four tiers, ending in a real incident.
- Warm-up5 min
Predict eight lines
One concept, guided. Near-impossible to fail.
- Challenge20 min
The equals that was ignored
Edge cases. You have to reason, and two valid fixes differ.
- Production incident45 min
The notifier that stopped notifying
A real incident: symptom first, cause hidden, tradeoff at the end.
- Interview replay10 min
Full round replay — overloading and overriding
Timed verbal replay with pass/fail criteria per follow-up.
Where this question goes next
Questions that lead here
When do you choose an abstract class over an interface?
Not on the old answer. Since Java 8 an interface carries behaviour, and since 9 it has private helper methods, so 'interfaces have no implementation' is out of date. Two differences remain: an abstract class can hold instance state and run a constructor, and you get only one. Choose it when subtypes must share state or a construction invariant; otherwise the interface.
Asked constantlyintermediate1–10 yrs11 min readOop
Every runnable example above was compiled and executed against openjdk 21.0.12 on this build, and its output diffed against what this page claims. Last updated 2026-09-13.