Home/Learn/Java A–Z/Polymorphism

Polymorphism

Beginner
OOP & Advanced Classes

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

PaymentDemo.java
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.

CastingDemo.java
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.

DispatchDemo.java
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

C = ClassA = AbstractI = Interface
CObject
Object is the root of the Java class hierarchy. Every class extends Object.
1 / 5

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 Aria
1

What is the difference between compile-time and runtime polymorphism in Java?

EasyTCS
2

What is dynamic dispatch? How does the JVM determine which method to call?

MediumOracle
3

Are fields polymorphic in Java? What about static methods?

MediumGoogle
4

What is the difference between upcasting and downcasting?

EasyAmazon
5

What happens if you call a downcast without an instanceof check?

EasyMicrosoft

Ask Aria about Polymorphism

Your personal AI tutor — ask anything about this concept