Why this matters
- Nearly every framework you will use — Spring, JDBC, the Collections Framework — works because of run-time overriding.
- The "which method runs" puzzle is a staple interview question, and the answer requires separating the two mechanisms cleanly.
- Static method hiding catches almost everyone once, and knowing it exists saves an afternoon.
Compile-time: overloading
Same name, different parameter lists. The compiler decides, using only the declared types at the call site.
class Printer {
void print(Object value) { System.out.println("Object"); }
void print(String value) { System.out.println("String"); }
}
Object text = "I am a String"; // declared Object, actually String
new Printer().print(text); // prints "Object"
The object is a String, but the variable is declared Object, and that is all the compiler looks
at. Overloading is never influenced by run-time type.
Run-time: overriding
Same signature, redefined in a subclass. The JVM decides, using the actual object.
class Notification {
void send() { System.out.println("Generic notification"); }
}
class EmailNotification extends Notification {
@Override
void send() { System.out.println("Email sent"); }
}
class PushNotification extends Notification {
@Override
void send() { System.out.println("Push delivered"); }
}
List<Notification> all = List.of(new EmailNotification(), new PushNotification());
for (Notification n : all) {
n.send(); // the object decides, not the reference type
}
Email sent
Push delivered
Decided before the program runs.
This is dynamic dispatch. The JVM consults the object's class to find the right method body — conceptually a lookup in a per-class method table. It is the mechanism that makes abstraction useful at run time.
The two side by side
| Aspect | Overloading | Overriding |
|---|---|---|
| Resolved | At compile time | At run time |
| Based on | Declared types of the arguments | Actual class of the object |
| Signature | Must differ | Must be identical |
| Return type | Free to differ | Same, or a subtype (covariant) |
| Where | Same class or a subclass | Subclass only |
| Access modifier | Unrestricted | Cannot be narrower than the parent's |
| Also called | Static or compile-time polymorphism | Dynamic or run-time polymorphism |
Resolved
OverloadingAt compile timeOverridingAt run timeBased on
OverloadingDeclared types of the argumentsOverridingActual class of the objectSignature
OverloadingMust differOverridingMust be identicalReturn type
OverloadingFree to differOverridingSame, or a subtype (covariant)Where
OverloadingSame class or a subclassOverridingSubclass onlyAccess modifier
OverloadingUnrestrictedOverridingCannot be narrower than the parent'sAlso called
OverloadingStatic or compile-time polymorphismOverridingDynamic or run-time polymorphism
Different names, different timing, different inputs to the decision.
Rules for a valid override
- Identical signature — same name, same parameter types in the same order.
- Return type must be the same or a subtype. Narrowing is allowed; widening is not.
- Access cannot narrow. A
publicmethod cannot be overridden asprotected. Widening is fine. - Checked exceptions cannot broaden. The override may throw fewer, narrower, or none — never a broader checked exception. Unchecked ones are unrestricted.
final,staticandprivatemethods cannot be overridden.privatemethods are not visible; the other two are not virtual.- Constructors are never overridden. They are not inherited in the first place.
Static method hiding
This is the trap. Static methods are not virtual, so redeclaring one in a subclass hides it rather than overriding it, and the reference type decides.
class Parent {
static String staticName() { return "Parent static"; }
String instanceName() { return "Parent instance"; }
}
class Child extends Parent {
static String staticName() { return "Child static"; }
@Override
String instanceName() { return "Child instance"; }
}
Parent reference = new Child();
System.out.println(reference.staticName()); // "Parent static" — reference type wins
System.out.println(reference.instanceName()); // "Child instance" — object type wins
Both lines use the same reference. The static call is bound at compile time from the declared type;
the instance call is dispatched at run time from the object. Calling a static method through an
instance reference is legal but misleading — always write Parent.staticName().
Fields behave the same way: they are hidden, not overridden, and resolved by reference type.
Upcasting and downcasting
Notification generic = new EmailNotification(); // upcast: implicit and always safe
EmailNotification email = (EmailNotification) generic; // downcast: checked at run time
Notification other = new PushNotification();
// EmailNotification wrong = (EmailNotification) other; // compiles, throws ClassCastException
if (other instanceof EmailNotification e) { // test and bind in one step
e.attachFile();
}
Upcasting to a supertype is always safe. Downcasting is a claim the compiler cannot verify, so the
JVM checks it and throws ClassCastException if the claim is false. The pattern-matching form of
instanceof shown above tests and binds in a single expression, and is covered fully in the Java
17 module.
Common misreadings
- "Overloading is a kind of polymorphism at run time." It is resolved entirely at compile time. Only overriding is dynamic.
- "Static methods can be overridden." They are hidden. The reference type decides which one runs.
- "An override can throw any exception." Checked exceptions may only narrow. Unchecked ones are unrestricted.
- "An override can reduce visibility." It cannot. Widening is permitted, narrowing is a compile error.
- "
@Overrideis required." It is optional but close to mandatory in practice, because it converts a silent mistake into a compile error. - "Fields are polymorphic." They are not. Field access is bound by reference type, like statics.
Quick recall
Everything you need if you only revisit this box.
- Overloading = compile time, decided by declared argument types, signatures must differ.
- Overriding = run time, decided by the actual object, signature must match exactly.
- Override rules: covariant return allowed, access cannot narrow, checked exceptions cannot broaden, and
final/static/privatecannot be overridden. - Always annotate with
@Overrideso the compiler checks you. staticmethods and fields are hidden, not overridden — reference type wins.- Upcasting is implicit and safe; downcasting is checked at run time and throws
ClassCastExceptionwhen wrong. - Long
instanceofchains are usually a missing polymorphic method.
Test yourself
Answer these before moving on — recall is what makes it stick.