PrepZone Logo
PrepZone

Polymorphism

Overloading resolves at compile time, overriding at run time — and method hiding fools everyone once.

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.

Java
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.

Java
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
}
Java
Email sent
Push delivered
Overloading — compile time
print(int)Chosen by argument type
print(String)Same name, different signature

Decided before the program runs.

Overriding — run time
Payment p = new UpiPayment()Declared type: Payment
UpiPayment.settle()Real object wins
The compiler picks between overloads from the declared types. The JVM picks between overrides from the real object, which is why a Payment variable can run CardPayment code.

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

AspectOverloadingOverriding
ResolvedAt compile timeAt run time
Based onDeclared types of the argumentsActual class of the object
SignatureMust differMust be identical
Return typeFree to differSame, or a subtype (covariant)
WhereSame class or a subclassSubclass only
Access modifierUnrestrictedCannot be narrower than the parent's
Also calledStatic or compile-time polymorphismDynamic or run-time polymorphism
  • Resolved

    OverloadingAt compile time
    OverridingAt run time
  • Based on

    OverloadingDeclared types of the arguments
    OverridingActual class of the object
  • Signature

    OverloadingMust differ
    OverridingMust be identical
  • Return type

    OverloadingFree to differ
    OverridingSame, or a subtype (covariant)
  • Where

    OverloadingSame class or a subclass
    OverridingSubclass only
  • Access modifier

    OverloadingUnrestricted
    OverridingCannot be narrower than the parent's
  • Also called

    OverloadingStatic or compile-time polymorphism
    OverridingDynamic 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 public method cannot be overridden as protected. 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, static and private methods cannot be overridden. private methods 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.

Java
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

Java
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.
  • "@Override is 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/private cannot be overridden.
  • Always annotate with @Override so the compiler checks you.
  • static methods and fields are hidden, not overridden — reference type wins.
  • Upcasting is implicit and safe; downcasting is checked at run time and throws ClassCastException when wrong.
  • Long instanceof chains are usually a missing polymorphic method.

Test yourself

Answer these before moving on — recall is what makes it stick.