JavaOOPbeginner
Updated:

Polymorphism in Java: Compile-Time vs Runtime

5 min read

One method name, many behaviors. Learn how overloading resolves at compile time, how overriding resolves at runtime, and why dynamic dispatch powers every real Java framework.

TL;DR – Quick Answer

Polymorphism in Java means one method call can produce different behavior depending on context. Compile-time polymorphism is method overloading: the compiler picks the method by its parameter list. Runtime polymorphism is method overriding: the JVM picks the version belonging to the actual object, not the reference type, through dynamic method dispatch.

On This Page

Ask ten freshers to define polymorphism and eight will recite "many forms" and stop there. That answer scores zero in an interview. What earns marks is explaining when the binding happens — at compile time for overloading, at runtime for overriding — and proving it with output you can predict.

Polymorphism builds directly on inheritance, so make sure you are comfortable with extends and overriding before diving in. Here we split the concept into its two forms, walk through dynamic dispatch line by line, and cover the trap questions.

The one-sentence version worth memorizing

Polymorphism lets the same method name trigger different behavior. Java delivers this in two ways:

Form Mechanism Who decides When
Compile-time (static) Method overloading Compiler While compiling
Runtime (dynamic) Method overriding JVM While running

Everything else in this article hangs off that table.

Compile-time polymorphism: method overloading

Overloading means several methods in the same class share a name but differ in their parameter lists — different count, different types, or different order of types. The compiler looks at the arguments in your call and binds the matching method before the program ever runs.

public class PaymentService {

    void pay(double amount) {
        System.out.println("Paying " + amount + " via default method");
    }

    void pay(double amount, String upiId) {
        System.out.println("Paying " + amount + " via UPI: " + upiId);
    }

    void pay(double amount, long cardNumber, int cvv) {
        System.out.println("Paying " + amount + " via card");
    }

    public static void main(String[] args) {
        PaymentService ps = new PaymentService();
        ps.pay(499.0);
        ps.pay(499.0, "student@upi");
        ps.pay(499.0, 4111111111111111L, 123);
    }
}

What to notice: three methods, one name, zero ambiguity — each call's argument list points to exactly one method. Changing only the return type is not overloading and will not compile, because the return type plays no part in the compiler's selection.

Common mistake: A common mistake beginners make is expecting pay(5) to be ambiguous between pay(double) and an imagined widening chain. Java resolves overloads in a fixed order: exact match, then widening (int → long → float → double), then autoboxing, then varargs. pay(5) widens int to double and compiles fine.

Runtime polymorphism: method overriding

Overriding happens across classes: a subclass redefines a method it inherited, keeping the same signature. The magic appears when a parent reference holds a child object.

class Notification {
    void send(String msg) {
        System.out.println("Generic notification: " + msg);
    }
}

class EmailNotification extends Notification {
    @Override
    void send(String msg) {
        System.out.println("Sending EMAIL: " + msg);
    }
}

class SmsNotification extends Notification {
    @Override
    void send(String msg) {
        System.out.println("Sending SMS: " + msg);
    }
}

public class Dispatcher {
    public static void main(String[] args) {
        Notification[] queue = {
            new EmailNotification(),
            new SmsNotification(),
            new Notification()
        };
        for (Notification n : queue) {
            n.send("Your class starts at 7 PM");
        }
        // Output:
        // Sending EMAIL: Your class starts at 7 PM
        // Sending SMS: Your class starts at 7 PM
        // Generic notification: Your class starts at 7 PM
    }
}

What to notice: the loop variable n is always typed Notification, yet each iteration runs a different send(). The compiler only verifies that Notification has a send(String) method; the JVM picks the actual body at runtime by checking the real object's class. That selection process is dynamic method dispatch.

This is not an academic trick. Every framework you will use professionally depends on it — Spring calls your controller through interface references, JDBC calls whichever driver's implementation is on the classpath. The caller codes against the parent type; the runtime supplies the child behavior.

Upcasting, downcasting, and reference vs object type

The line Notification n = new EmailNotification(); is an upcast — child object, parent reference. It is always safe and needs no cast operator. Rules that follow from it:

  • You can call only methods declared in the reference type. n.someEmailOnlyMethod() will not compile even though the object has it.
  • Overridden methods run the object type's version (dynamic dispatch).
  • To reach child-only members, downcast — and guard it with instanceof:
if (n instanceof EmailNotification email) {  // pattern matching, Java 16+
    email.attachInvoice();
}

Pro tip: If you find yourself writing long instanceof chains, that is a design smell. Push the varying behavior into an overridden method so dispatch does the branching for you — that is literally what polymorphism is for.

Overloading vs overriding: the comparison interviewers want

Dimension Overloading Overriding
Where Same class (or inherited into it) Parent–child classes
Signature Must differ in parameters Must be identical
Return type Can be anything Same or covariant subtype
Access modifier Anything Cannot be narrower than parent's
Binding Compile time Runtime
static methods Can be overloaded Cannot be overridden (only hidden)
private methods Can be overloaded Not visible, so not overridable
Exceptions No restriction Cannot throw broader checked exceptions

The traps: fields and static methods do not dispatch

Only instance methods are polymorphic. Fields and static methods resolve by reference type at compile time.

class Parent {
    String label = "parent-field";
    static String tag() { return "parent-static"; }
}

class Child extends Parent {
    String label = "child-field";
    static String tag() { return "child-static"; }
}

public class TrapDemo {
    public static void main(String[] args) {
        Parent p = new Child();
        System.out.println(p.label);   // parent-field
        System.out.println(p.tag());   // parent-static
    }
}

What to notice: both lines print the parent version despite the object being a Child. Field access and static calls are bound to the declared type — this exact snippet appears in written tests more often than any other polymorphism question.

Interview note: When asked "predict the output," first check whether the member is an instance method. If it is not, ignore the object type entirely and answer from the reference type. This single rule solves most trick questions on our Java OOP interview questions page.

Where polymorphism meets abstraction

Runtime polymorphism becomes truly powerful when the parent type is abstract — an abstract class or interface that cannot be instantiated and exists purely as a contract. List<String> list = new ArrayList<>(); is polymorphism through an interface, and it is the reason you can swap ArrayList for LinkedList in one line. Read Abstraction in Java next to complete that half of the picture, and see how it plays out at scale in the Java Collections Framework.

Practice checklist

Before moving on, you should be able to do these without notes:

  1. Write one class with three overloads of the same method and predict which each call binds to.
  2. Write a parent-reference/child-object example and explain the output using the words "dynamic method dispatch."
  3. State the two members that never dispatch dynamically (fields, static methods).
  4. Explain why @Override on a hiding static method is a compile error.

If you can, you are ahead of most candidates. To drill this with mentors and mock interviews rather than alone, this is core material in our Java Full Stack course.

Frequently Asked Questions

What are the two types of polymorphism in Java?
Compile-time (static) polymorphism, achieved through method overloading, where the compiler selects the method based on the number and types of arguments. And runtime (dynamic) polymorphism, achieved through method overriding, where the JVM selects the method based on the actual object type at execution time.
What is dynamic method dispatch?
It is the mechanism the JVM uses to decide which overridden method to run when a superclass reference points to a subclass object. The decision happens at runtime using the object's actual class, not the reference's declared type. This is what makes code like Animal a = new Dog(); a.sound(); print the dog's sound.
Is method overloading really polymorphism?
Yes, but it is the static form. The same method name behaves differently based on the argument list, and the binding is finalized by the compiler before the program runs. Some interviewers argue only overriding is true polymorphism; the safe answer is to name both forms and clearly state when each one binds.
Can we override static methods in Java?
No. Static methods belong to the class, not to objects, so they do not participate in dynamic dispatch. Declaring a static method with the same signature in a subclass is called method hiding, and the version invoked depends on the reference type at compile time rather than the object type at runtime.
Why can't we overload methods by changing only the return type?
Because the compiler resolves an overloaded call by looking at the arguments you pass, and the return type is not part of that decision. If two methods differed only by return type, a call like process(5) would be ambiguous, so Java rejects it at compile time.
Does polymorphism apply to fields (variables) in Java?
No. Fields are resolved by the reference type at compile time, never by the object type. If a parent and child both declare a field named count, a parent reference always reads the parent's field even when it points to a child object. Only instance methods are dispatched dynamically.

Want to Build Your Career in Java Full Stack with AI?

Join CodeBegun and train with working industry engineers — Explore the Program

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 14 July 2026 LinkedIn
Chat with us