Polymorphism
BeginnerWrite code that works on any subtype with compile-time overloading and runtime dynamic dispatch — the most powerful tool in OOP.
Overview
Polymorphism means 'many forms'. In Java there are two kinds: compile-time (static) polymorphism through method overloading, where the compiler selects the method based on argument types; and runtime (dynamic) polymorphism through method overriding, where the JVM selects the method based on the actual object type at runtime. Runtime polymorphism is what makes frameworks, collections, and the Strategy pattern possible — you program to a supertype and let subclasses provide the behaviour.
Runtime Polymorphism & Dynamic Dispatch
When you store a subclass object in a supertype variable and call an overridden method, Java always executes the subclass's version — this is dynamic dispatch (or late binding). The decision is made at runtime based on the heap object's actual type, not the variable's declared type.
This is the key to writing extensible code: a method that accepts a Shape works for Circle, Rectangle, and any future shape you add without changing the method.
abstract class Payment {
protected double amount;
public Payment(double amount) { this.amount = amount; }
public abstract String process(); // each subclass processes differently
public void printReceipt() {
System.out.println("Receipt: " + process() + " — $" + amount);
}
}
class CreditCard extends Payment {
private String last4;
public CreditCard(double amt, String last4) { super(amt); this.last4 = last4; }
@Override public String process() { return "Credit card *" + last4; }
}
class PayPal extends Payment {
private String email;
public PayPal(double amt, String email) { super(amt); this.email = email; }
@Override public String process() { return "PayPal (" + email + ")"; }
}
class Crypto extends Payment {
private String wallet;
public Crypto(double amt, String wallet) { super(amt); this.wallet = wallet; }
@Override public String process() { return "Crypto wallet " + wallet; }
}
public class PaymentDemo {
// Works for ALL Payment subtypes — past, present, and future
static void checkout(Payment payment) {
payment.printReceipt(); // dynamic dispatch selects correct process()
}
public static void main(String[] args) {
Payment[] payments = {
new CreditCard(99.99, "4242"),
new PayPal(49.00, "user@example.com"),
new Crypto(200.00, "0x1A2B..."),
};
for (Payment p : payments) checkout(p);
// Receipt: Credit card *4242 — $99.99
// Receipt: PayPal (user@example.com) — $49.0
// Receipt: Crypto wallet 0x1A2B... — $200.0
}
}Upcasting, Downcasting & instanceof
Upcasting (subtype → supertype) is always safe and implicit — a Dog IS-A Animal. Downcasting (supertype → subtype) requires an explicit cast and throws ClassCastException at runtime if wrong.
Always guard a downcast with instanceof. Java 16+ pattern-matching instanceof combines the check and cast in one expression, eliminating the redundant cast.
public class CastingDemo {
public static void main(String[] args) {
// Upcasting — implicit, always safe
Animal a = new Dog("Rex"); // Dog reference stored in Animal variable
// Downcasting — explicit, can fail
if (a instanceof Dog dog) { // pattern matching (Java 16+)
System.out.println(dog.fetch()); // safe — no separate cast needed
}
// Old style (pre-Java 16)
if (a instanceof Dog) {
Dog d = (Dog) a; // explicit cast after check
System.out.println(d.fetch());
}
// ClassCastException without instanceof guard
Animal cat = new Cat("Whiskers");
try {
Dog badCast = (Dog) cat; // throws ClassCastException
} catch (ClassCastException e) {
System.out.println("Cannot cast Cat to Dog: " + e.getMessage());
}
// getClass() — exact type, not polymorphic
System.out.println(a.getClass().getSimpleName()); // Dog
System.out.println(a instanceof Animal); // true
System.out.println(a instanceof Dog); // true
System.out.println(a instanceof Cat); // false
}
}Compile-Time vs Runtime Polymorphism
Compile-time (static) polymorphism = method overloading. The compiler picks the method based on the declared parameter types. Even if you pass a subtype, the declared type of the variable determines which overload is chosen — not the runtime type.
Runtime (dynamic) polymorphism = method overriding. The JVM picks the method based on the actual heap object type.
Fields and static methods are NOT polymorphic — they are resolved by the declared type at compile time.
public class DispatchDemo {
static class Base {
String name = "Base";
static String staticName() { return "Base-static"; }
String instanceMethod() { return "Base-instance"; }
}
static class Sub extends Base {
String name = "Sub"; // hides field, does NOT override
static String staticName() { return "Sub-static"; } // hides, NOT override
@Override String instanceMethod() { return "Sub-instance"; } // TRUE override
}
// Overloaded methods — compile-time dispatch
static void print(Base b) { System.out.println("print(Base)"); }
static void print(Sub s) { System.out.println("print(Sub)"); }
public static void main(String[] args) {
Base obj = new Sub(); // declared type: Base, runtime type: Sub
// Instance method — runtime dispatch (dynamic)
System.out.println(obj.instanceMethod()); // Sub-instance ✓
// Field access — compile-time (static), uses declared type
System.out.println(obj.name); // Base (not Sub!)
// Static method — compile-time, uses declared type
System.out.println(Base.staticName()); // Base-static (not Sub!)
// Overloaded method — compile-time dispatch on declared type
print(obj); // print(Base) — because declared type is Base
print((Sub) obj); // print(Sub) — after downcast
}
}Interactive Visualization
Key Points to Remember
- Runtime polymorphism: method dispatch is based on the actual object type, not the variable's declared type
- Fields and static methods are NOT polymorphic — they are resolved by the declared (compile-time) type
- Upcasting is always safe and implicit; downcasting requires instanceof guard + explicit cast
- Pattern-matching instanceof (Java 16+) combines the check and cast: if (a instanceof Dog dog)
- Overloaded method selection is based on the declared parameter types at compile time
- Programming to a supertype (interface/abstract class) lets new subtypes be added without changing callers
Practice Polymorphism in the Playground
Run and modify code directly in your browser - no setup needed.
Interview Questions
Sign in to ask AriaWhat is the difference between compile-time and runtime polymorphism in Java?
What is dynamic dispatch? How does the JVM determine which method to call?
Are fields polymorphic in Java? What about static methods?
What is the difference between upcasting and downcasting?
What happens if you call a downcast without an instanceof check?
Ask Aria about Polymorphism
Your personal AI tutor — ask anything about this concept