Topic 61 of 119 6. Object-Oriented Programming in Java Interactive Tutorial

Polymorphism in Java: Compile-Time vs Runtime Polymorphism

18 mins read Last updated: 22 Aug 2026 29,874 views

Direct Answer & Core Concept

Polymorphism means one name behaving differently depending on context. Java splits it into two mechanisms resolved at two different times, and the distinction is not academic because the two fail in completely different ways. Compile-time (static) polymorphism is method overloading. Several methods share a name and differ in parameter list, and javac picks one using the declared types of the arguments.
Main.java
Run Online
print("hello");
// picks print(String)
print(42);
// picks print(int)
If no overload matches, you get a compile error. Nothing about it can change later. Runtime (dynamic) polymorphism is method overriding. A parent-typed reference holds a child object, and the JVM calls the override on the actual object rather than the declared type.
Main.java
Run Online
Shape s = new Circle();
s.draw();
// calls Circle.draw, decided at runtime
Here the compiler accepts the call against Shape and the decision happens when the program runs.
AspectCompile-timeRuntime
MechanismOverloadingOverriding
Decided byThe compilerThe JVM
UsesParameter typesActual object type
NeedsNothing specialAn inheritance relationship
Fails asCompile errorSilent wrong behaviour
The practical consequence is that overriding is a design tool and overloading is a convenience. Overloading is chosen by whoever wrote the call and can never surprise them, while overriding changes the behaviour of code that was written earlier and compiled earlier.

Detailed Explanation & Mechanics

1) One Name, Two Resolutions

Polymorphism is a single idea applied at two different stages. Java keeps them separate deliberately, because mixing them would make a program impossible to reason about.


Diagram contrasting compile-time overload resolution against runtime override dispatch
The compiler picks an overload from declared types; the JVM picks an override from the real object.

Overloading happens entirely inside one class. Overriding always crosses a class boundary, which is why it needs inheritance to exist at all.

2) Compile-Time Polymorphism Is Overloading

Several methods share a name but differ in parameter list. The compiler selects one from the declared types of the arguments, and the choice is baked into the bytecode.

Main.java
Run Online
public class Printer {
    public void print(String text) {
        System.out.println("string: " + text);
    } public void print(int number) {
        System.out.println("int: " + number);
    } public void print(int number, int times) {
        System.out.println("int repeated " + times + " times: " + number);
    }
}
Main.java
Run Online
Printer p = new Printer();
p.print("hello");
// picks print(String)
p.print(SMALL_NUMBER);
// picks print(int)
p.print(REPEATED, COUNT);
// picks the two-argument version

Add a call that matches nothing and the compiler rejects it. That is the safety property: an unresolvable overload can never reach runtime. The overloading topic covers the resolution rules in full.

3) The Widening Trap

Because resolution uses declared types rather than values, a call can resolve to an overload you did not expect.

Main.java
Run Online
public class Printer {
    public void print(String text) {
        System.out.println("string overload");
    } public void print(Object value) {
        System.out.println("object overload");
    } public void print(Integer value) {
        System.out.println("integer overload");
    }
}// An ALREADY BOXED Integer matches print(Integer) exactly
print(Integer.valueOf(SMALL_NUMBER));
// A PRIMITIVE int cannot reach Integer without boxing, so it WIDENS
// to Object instead, and print(Object) wins
print(SMALL_NUMBER);

The rule is that widening beats boxing. Method resolution tries the applicable methods in phases, and an exact or widening match in an earlier phase beats a boxing conversion in a later one.

This is why passing an Integer to an overloaded method taking both Object and Integer selects the Integer version, while passing a primitive int selects neither and widens to Object. The two calls look alike and behave differently.

4) Runtime Polymorphism Is Overriding

A method marked @Override replaces an inherited one with the same signature. When called through a parent-typed reference, the JVM uses the actual object type.

Main.java
Run Online
public class Shape {
    public void draw() {
        System.out.println("drawing a generic shape");
    }
}public class Circle extends Shape {
    @Override public void draw() {
        System.out.println("drawing a circle");
    }
}
Main.java
Run Online
Shape reference = new Circle();
reference.draw();
// the compiler checks against Shape,
// but the JVM runs Circle.draw

The compiler only verifies that Shapehas a draw method. Which one runs is settled at the moment of the call, using the object reference on the heap.

5) Why a Reference Can Hold a Child

Assignment is allowed whenever the child satisfies the parent contract, because every Circle really is a Shape. The variable type is a promise about what can be called; the object type is what actually happens.

Main.java
Run Online
Shape s = new Circle();
// upcast, always safe
Circle c =(Circle) s;
// downcast, checked at runtime

Only the reference type matters for compilation. If you assign a Circle to a Shape variable, the compiler will reject s.getRadius() even though the object really is a circle. That is a compile-time restriction, not a runtime one.

6) The Case Where Overriding Does Not Apply

Dynamic dispatch applies to instance methods. A static method is resolved by the compiler using the reference type and cannot be overridden at all.

Main.java
Run Online
public class Base {
    public static void announce() {
        System.out.println("base static");
    } public void speak() {
        System.out.println("base instance");
    }
}public class Derived extends Base {
    public static void announce() {
        System.out.println("derived static");
    } @Override public void speak() {
        System.out.println("derived instance");
    }
}
Main.java
Run Online
Base b = new Derived();
b.speak();
// derived instance, dynamic dispatch applies
b.announce();
// base static, bound by the reference type
Derived.announce();
// derived static, called explicitly

The same applies to private methods, which cannot be overridden because a subclass cannot see them, and to final methods, which forbid overriding at compile time.

7) Fields Are Bound Statically Too

Field access never uses the actual object type. It is resolved at compile time from the reference, so a field hidden in a subclass does not participate in polymorphism.

Main.java
Run Online
public class Parent {
    protected String label = "parent";
}public class Child extends Parent {
    protected String label = "child";
    // hides, does not override
}Parent reference = new Child();
System.out.println(reference.label);
// parent, bound at compile time

This is a common source of confusion, because the rule for methods and the rule for fields differ. Methods dispatch on the object; fields bind to the declared type. Overriding a field is not possible in Java, and trying to do it produces two separate variables rather than polymorphism.

8) The Array Advantage

Runtime polymorphism is what makes it possible to handle a mixed collection without any type tests, and the pattern is worth internalising.

Main.java
Run Online
Shape circle = new Circle("circle", RADIUS);
Shape square = new Square("square", SIDE);
Shape[] shapes = {
    circle, square
};
for (Shape shape : shapes) {
    shape.draw();
    // the correct override for each object
}

No instanceof chain, no switch on a type tag, and adding a new subtype later requires no change to the loop. This is the open-closed principle in practice: the collection is closed for modification, and the loop is open for extension.

ApproachCost of a new subtype
if or switch on typeEdit every branch
instanceof chainEdit every chain
Polymorphic callAdd the class only

9) Programming to the Supertype

The payoff is generic code that works with any subtype, including ones written after yours. This is why dependencies should be declared against the supertype.

Main.java
Run Online
// The method depends on the interface, so any implementation works
public void printAll(List<String> items) {
    for (String item : items) {
        System.out.println(item);
    }
}printAll(new ArrayList<>());
printAll(new LinkedList<>());
printAll(List.of("a", "b"));

Every call site works without the method knowing which implementation it received. Swap the argument and behaviour can change entirely, which is the practical value of dynamic dispatch. The interfaces topic covers why interfaces are the usual supertype here.

Prefer declaring variables and parameters at the supertype, but construct them at the concrete type. Writing ArrayList<String> list = new LinkedList<>() gains nothing and needlessly restricts the variable, because only ArrayList methods become callable on it.

10) Interface Defaults and the Diamond

An interface may supply a default implementation. If a class inherits two defaults with the same signature, the compiler refuses to choose, and you must resolve it explicitly.

Main.java
Run Online
public interface Printable {
    default void print() {
        System.out.println("printable");
    }
}public interface Archivable {
    default void print() {
        System.out.println("archivable");
    }
}public class Report implements Printable, Archivable {
    @Override public void print() {
        // REQUIRED, otherwise the class does not compile
        Printable.super.print();
        Archivable.super.print();
    }
}

This is the same ambiguity that stops classes inheriting two parents, showing up in the one place Java allows multiple inheritance of type.

11) Common Misconceptions

ClaimVerdict
Overloading and overriding are the sameFalse. One class versus two.
Overloading picks by argument valuesFalse. By declared types.
Overriding uses the reference typeFalse. It uses the actual object.
Static methods are overriddenFalse. They are hidden, not overridden.
Fields are overridden polymorphicallyFalse. They bind to the reference.
Overriding requires an interfaceFalse. Any superclass works.
Overloading can fail at runtimeFalse. It fails at compile time.
Downcasting is needed to get the overrideFalse. The JVM does it for you.

Frequently Asked Questions

What is the real difference between static and dynamic polymorphism?


Static polymorphism is overloading, where the compiler chooses a method from the declared types of the arguments, and the decision is final once compiled. Dynamic polymorphism is overriding, where the JVM chooses the implementation from the actual object type when the call executes. The practical difference is how they fail: an unresolvable overload is a compile error, whereas an unwanted override is silently wrong behaviour.

Why does a call pick Object rather than Integer?


Because method resolution runs in phases, and widening beats boxing. Passing a primitive int cannot become an Integer without boxing, so widening to Object is the applicable match found in the earlier phase. Passing an already boxed Integer can match directly, so the Integer overload wins for that call.

Does overriding work with static methods?


No. A static method is bound at compile time by the reference type and is hidden rather than overridden. The same is true of private methods, which a subclass cannot see, and final methods, which forbid it outright. Only instance methods participate in dynamic dispatch.

Why does a field not behave polymorphically?


Because field access is resolved at compile time from the reference type, with no runtime lookup involved. A subclass field with the same name hides the parent one instead of replacing it, leaving two separate variables in the object. Methods dispatch on the object; fields bind to the declaration.

How do I avoid instanceof checks over a collection?


Declare the loop variable at the supertype and call a method declared there. Each object then runs its own override without any type test, and adding a subtype later needs no change to the loop. That is the main practical benefit of runtime polymorphism, and it is why the pattern is preferred over type branching.

Should I declare variables at the supertype or the concrete type?


Declare parameters and locals at the supertype and construct at the concrete type. A variable typed as the concrete class restricts which methods are callable and gains nothing, whereas a supertype-typed variable accepts any implementation and still dispatches to the right override at runtime.

What happens when two interfaces supply the same default?


The compiler reports a conflict, because it will not choose between them. The implementing class must override the method and may call either parent explicitly with a qualified super call. This is the diamond ambiguity, which is precisely why classes may extend only one parent while interfaces may multiply inherit.

Is operator overloading part of this?


No, and its absence is worth noting. Java does not let you define operator behaviour, so plus on two objects means concatenation for strings and addition for numbers, with no polymorphic dispatch. Overloading in Java always means method overloading chosen by the compiler.

Runnable Code Example: Polymorphism in Java

Java 17+
Main.java
Run Online
// Shows compile-time resolution by declared type against runtime
// resolution by actual object, and the cases where neither applies.
public class PolymorphismDemo {
    private static final int REP_COUNT = 3;
    private static final int SMALL_NUMBER = 42;
    private static final double RADIUS = 2.0;
    private static final double SIDE = 3.0;
    // A BASE TYPE, ITS OVERRIDE IS DECIDED BY THE JVM
    public static class Shape {
        private final String name;
        public Shape(String name) {
            this.name = name;
        } public String getName() {
            return name;
        } public void draw() {
            System.out.println("drawing a generic " + name);
        }
    } public static class Circle extends Shape {
        private final double radius;
        public Circle(String name, double radius) {
            super(name);
            this.radius = radius;
        } @Override public void draw() {
            System.out.println("drawing a " + getName() + " with radius " + radius);
        }
    } public static class Square extends Shape {
        private final double side;
        public Square(String name, double side) {
            super(name);
            this.side = side;
        } @Override public void draw() {
            System.out.println("drawing a " + getName() + " with side " + side);
        }
    } // COMPILE-TIME POLYMORPHISM, RESOLVED BY DECLARED ARGUMENT TYPES
    public static class Printer {
        public void print(String text) {
            System.out.println("print(String): " + text);
        } public void print(int number) {
            System.out.println("print(int): " + number);
        } public void print(int number, int times) {
            System.out.println("print(int, int): " + number + " repeated " + times);
        } // This overload exists only to demonstrate the widening trap
        public void print(Object value) {
            System.out.println("print(Object): " + value);
        }
    } // A STATIC METHOD IS NEVER OVERRIDDEN, IT IS HIDDEN
    public static class Base {
        public static void announce() {
            System.out.println("Base.announce");
        } public void speak() {
            System.out.println("Base.speak");
        } protected String label = "base";
    } public static class Derived extends Base {
        public static void announce() {
            System.out.println("Derived.announce");
        } @Override public void speak() {
            System.out.println("Derived.speak");
        } // A FIELD HIDES RATHER THAN OVERRIDES, SO THIS IS A SECOND VARIABLE
        protected String label = "derived";
    } // THE SUPERTYPE IS ALL THAT IS NEEDED FOR A POLYMORPHIC LOOP
    public static void renderAll(Shape[] shapes) {
        for (Shape shape : shapes) {
            shape.draw();
        }
    } public static void main(String[] args) {
        // COMPILE-TIME, EACH CALL PICKS ITS OVERLOAD NOW
        Printer printer = new Printer();
        printer.print("hello");
        printer.print(SMALL_NUMBER);
        printer.print(REP_COUNT, REP_COUNT);
        // THE WIDENING TRAP, AN int CANNOT BOX, SO IT WIDENS TO Object
        printer.print((Object) SMALL_NUMBER);
        // RUNTIME, ONE LOOP PRODUCES THREE DIFFERENT BEHAVIOURS
        Shape circle = new Circle("circle", RADIUS);
        Shape square = new Square("square", SIDE);
        Shape generic = new Shape("blob");
        System.out.println();
        System.out.println("runtime dispatch:");
        circle.draw();
        square.draw();
        generic.draw();
        // NO TYPE TESTS, AND A NEW SUBTYPE NEEDS NO CHANGE HERE
        System.out.println();
        System.out.println("polymorphic loop:");
        renderAll(new Shape[] {
            circle, square, generic
        });
        // STATIC METHODS BIND TO THE REFERENCE TYPE, NOT THE OBJECT
        System.out.println();
        System.out.println("static binding:");
        Base asBase = new Derived();
        asBase.speak();
        // derived, instance method dispatches
        asBase.announce();
        // base, static method binds statically
        Derived.announce();
        // derived, called explicitly by name
        // FIELDS ALSO BIND TO THE REFERENCE TYPE
        System.out.println();
        System.out.println("field binding:");
        System.out.println("asBase.label:     " + asBase.label);
        Derived asDerived =(Derived) asBase;
        System.out.println("asDerived.label:  " + asDerived.label);
        System.out.println("asBase.speak:     " + asBase.getClass().getSimpleName());
        /*
            ACTUAL OUTPUT:

                print(String): hello
                print(int): 42
                print(int, int): 3 repeated 3
                print(Object): 42

            runtime dispatch:
                drawing a circle with radius 2.0
                drawing a square with side 3.0
                drawing a generic blob

            polymorphic loop:
                drawing a circle with radius 2.0
                drawing a square with side 3.0
                drawing a generic blob

            static binding:
                Derived.speak
                Base.announce
                Derived.announce

            field binding:
                asBase.label:     base
                asDerived.label:  derived
                asBase.speak:     Derived

            THE SIX DISTINCTIONS THAT MATTER MOST
            1) OVERLOADING IS CHOSEN BY DECLARED TYPES. The compiler
               makes the decision and bakes it in, so a call that
               matches nothing is a compile error rather than a
               runtime surprise.
            2) OVERRIDING IS CHOSEN BY THE ACTUAL OBJECT. The compiler
               only checks that the supertype declares the method,
               and the JVM performs the lookup when the call runs.
            3) WIDENING BEATS BOXING. A primitive int widens to Object
               without conversion, while reaching Integer needs a
               box, so the earlier phase wins and Object is chosen.
            4) STATIC METHODS AND FIELDS DO NOT DISPATCH. Both bind to
               the reference type at compile time, so asBase.announce
               runs the base version and asBase.label reads base.
            5) FIELDS CANNOT BE OVERRIDDEN. A same-named child field
               hides the parent one, leaving two separate variables
               in the same object rather than one polymorphic slot.
            6) THE PAYOFF IS EXTENSIBILITY. The renderAll method was
               written once and handles every subtype, so adding a
               Triangle later requires no change to the loop at all.
        */
    }
}

Common Java Technical Interview Questions & Answers

Interview Prep
Q1: What is the main difference between static and dynamic polymorphism?\nA1: Static polymorphism is overloading, where the compiler picks a method from the declared types of the arguments and the decision is final once compiled. Dynamic polymorphism is overriding, where the JVM picks the implementation from the actual object type at the moment of the call. The practical difference is how they fail: an unresolvable overload is a compile error, whereas an unwanted override is silently wrong behaviour.\nQ2: Why does an overloaded call sometimes pick Object over Integer?\nA2: Because method resolution runs in phases and widening beats boxing. A primitive int cannot become an Integer without a boxing conversion, so widening to Object is found in an earlier phase and wins. Passing an already boxed Integer can match directly, so the Integer overload is selected for that call instead.\nQ3: What enables runtime polymorphism in Java?\nA3: Dynamic method dispatch in the JVM, implemented through the virtual method table of each class. When an instance method is invoked through a reference, the JVM looks up the implementation on the actual object rather than the declared type, which is why an override declared in a subclass runs.\nQ4: Can static methods be overridden?\nA4: No. A static method is bound at compile time by the reference type, so a same-named method in a subclass hides rather than overrides it. Private methods cannot be overridden either, because a subclass cannot see them, and final methods forbid it outright. Only instance methods dispatch dynamically.\nQ5: Why do fields not participate in polymorphism?\nA5: Field access is resolved at compile time from the declared reference type, with no runtime lookup. A subclass field with the same name hides the parent field instead of replacing it, so both variables exist in the same object. Methods dispatch on the object, whereas fields bind to the declaration.\nQ6: How do you avoid instanceof checks when processing a collection?\nA6: Declare the loop variable at the supertype and call a method declared there, so each object runs its own override without any type test. Adding a subtype later then requires no change to the loop. That is the main practical benefit of runtime polymorphism, and it is why this pattern is preferred over type branching.\nQ7: Should a variable be declared at the concrete type or the supertype?\nA7: Declare parameters and locals at the supertype and construct at the concrete type. A variable typed as the concrete class restricts which methods are callable and gains nothing, while a supertype-typed variable accepts any implementation and still dispatches to the correct override at runtime.\nQ8: What happens when two interfaces declare the same default method?\nA8: The compiler reports a conflict, because it will not choose between the two implementations. The implementing class must override the method and may call either parent explicitly using a qualified super call. This is the diamond ambiguity that prevents classes from inheriting two parents while interfaces may multiply inherit.\nQ9: Is operator overloading part of polymorphism in Java?\nA9: No, and Java does not support it. The plus operator on objects means concatenation for strings and addition for numbers, with no user-defined behaviour and no polymorphic dispatch. Overloading in Java always means method overloading chosen by the compiler from declared argument types.

Key Takeaways, Best Practices & Clean Code Rules

Java Best Practice & Production Rules

Declare parameters and locals at the supertype, construct at the concrete type.\nUse runtime polymorphism instead of instanceof or switch on a type tag.\nKeep new subtypes from requiring edits to existing loops or branches.\nRemember overloading resolves on declared types, not on runtime values.\nAdd the @Override annotation to every overriding method.\nDo not expect static methods or fields to dispatch polymorphically.\nAvoid hiding fields, since a same-named child field creates a second variable.\nBe wary of overloads that differ only by boxing versus widening.\nName overloads by role rather than adding near-identical signatures.\nPrefer polymorphism for open-closed behaviour, composition for changeable parts.\nProgram to interfaces so implementations stay replaceable.\nHandle the compile error for conflicting interface defaults deliberately.\nDo not downcast just to reach an override, since dispatch already handles it.\nUse @Override so a renamed parent method fails loudly rather than silently.\nKeep overriding methods consistent with the supertype contract.\nRemember an unwanted override is silent, unlike an unmatched overload.
We value your privacy & transparency

OperateTools uses cookies and local storage to ensure basic tool functionality, preserve preferences, and analyze aggregated traffic. We never sell your personal data or index your document files. Learn more in our Privacy Policy.