Java Polymorphism Explained: Compile-Time vs Runtime with Examples

Java Polymorphism Explained: Compile-Time vs Runtime with Examples


In this beginner-friendly guide, you will learn Java polymorphism compile-time vs runtime examples with clear code you can run today. If you are a student or new to Java OOP, this tutorial explains what polymorphism is, why it matters, and how to use method overloading and overriding step by step. By the end, you will know the difference between compile time and runtime polymorphism in Java, how to avoid common mistakes, and how modern Java (21+) makes polymorphism safer and easier.

What is polymorphism in Java? (For beginners)

Java polymorphism compile-time example: visualizing method overloading resolution
Compile-time polymorphism shown via method overloading resolved before execution.

Polymorphism means “many forms.” In Java OOP, it lets the same method name behave differently depending on its input or the actual object type. In simple words: you write one interface or method name, and Java decides the correct behavior based on context.

  • Compile-time polymorphism (method/constructor overloading): The compiler selects a method based on parameter types and count. Decision happens before the program runs.
  • Runtime polymorphism (method overriding): The JVM selects which overridden instance method to call based on the object’s real type at runtime (dynamic dispatch).

Why use polymorphism? It reduces duplication, keeps your code flexible, and lets you “program to an interface.” This is essential for building clean, testable Java applications.

How Java picks a method: compile-time vs runtime
Compile-time (Overloading)
Call site has method name + arguments
Compiler applies overload resolution

Exact match → widening → boxing/unboxing → varargs. Most specific wins.

Chosen method signature is baked into bytecode (call fixed)

Runtime (Overriding)
Reference type is supertype/interface
Actual object type at runtime (e.g., Dog, Cat)
JVM performs dynamic dispatch → calls overridden method of real object

Tip for students: If you change only parameters, it’s an overload (compile-time). If you change behavior in a subclass with the same signature, it’s an override (runtime).

Compile-time polymorphism in Java (method overloading)

Java polymorphism runtime example: method overriding and dynamic dispatch visual
Runtime polymorphism with overriding and late binding when invoking through a base reference.

Method overloading happens when a class has multiple methods with the same name but different parameter lists. The compiler decides which overload to call based on the argument types and number. Return type alone does not distinguish overloads.

Simple overloading example

Code
class Printer {
    void print(int n) {
        System.out.println("Printing int: " + n);
    }

    void print(String s) {
        System.out.println("Printing String: " + s);
    }

    void print(double d, int times) {
        System.out.println("Printing double " + d + ", " + times + " times:");
        for (int i = 0; i < times; i++) {
            System.out.print(d + " ");
        }
        System.out.println();
    }
}

class="cd-keyword cd-access">public class OverloadDemo {
    class="cd-keyword cd-access">public static void main(String[] args) {
        Printer p = new Printer();
        p.print(5);             // calls print(int)
        p.print("Hello");       // calls print(String)
        p.print(3.14, 3);       // calls print(double, int)

        // char promotes to int; the most specific overload is chosen
        p.print('A');           // calls print(int) and prints its numeric value 65
    }
}
Output
Printing int: 5
Printing String: Hello
Printing double 3.14, 3 times:
3.14 3.14 3.14 
Printing int: 65

Constructor overloading

Constructors can be overloaded too (different parameter lists). Constructors are not inherited and cannot be overridden.

Code
class Student {
    String name;
    int age;

    Student(String name) {
        this.name = name;
        this.age = 0;
    }

    Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    void info() {
        System.out.println(name + " (" + age + ")");
    }
}

class="cd-keyword cd-access">public class ConstructorOverload {
    class="cd-keyword cd-access">public static void main(String[] args) {
        Student a = new Student("Anita");
        Student b = new Student("Rohan", 19);
        a.info();
        b.info();
    }
}
Output
Anita (0)
Rohan (19)

How the compiler chooses an overload

  • Match by exact parameter types first.
  • Then by widening conversions if needed (e.g., char to int, int to long).
  • Autoboxing/unboxing may apply (e.g., int to Integer).
  • Most specific applicable method wins.
  • Return type is ignored during overload resolution.

Common overloading pitfalls

  • Ambiguous calls: similar overloads can confuse the compiler.
  • Expecting return type to distinguish overloads (it does not).
Code
class Ambiguity {
    void test(Integer x) { System.out.println("Integer"); }
    void test(Long x)    { System.out.println("Long"); }

    class="cd-keyword cd-access">public static void main(String[] args) {
        Ambiguity a = new Ambiguity();
        // a.test(null); // Compile error: reference to test is ambiguous
        a.test((Integer) null); // OK: picks Integer overload
    }
}

Runtime polymorphism in Java (method overriding)

Method overriding happens when a subclass provides its own implementation of an inherited instance method with the same signature. At runtime, Java picks the method based on the object’s actual type, not the reference type. This is called dynamic dispatch and is the core of runtime polymorphism.

Simple runtime polymorphism example using inheritance

Code
class Animal {
    void speak() {
        System.out.println("Animal speaks");
    }
}

class Dog extends Animal {
    class="cd-annotation">@Override
    void speak() {
        System.out.println("Dog barks");
    }
}

class Cat extends Animal {
    class="cd-annotation">@Override
    void speak() {
        System.out.println("Cat meows");
    }
}

class="cd-keyword cd-access">public class RuntimePolymorphismDemo {
    class="cd-keyword cd-access">public static void main(String[] args) {
        Animal a1 = new Dog();  // upcasting
        Animal a2 = new Cat();

        a1.speak(); // Dog barks (decided at runtime)
        a2.speak(); // Cat meows

        Animal[] animals = { new Dog(), new Cat(), new Animal() };
        for (Animal a : animals) {
            a.speak();
        }
    }
}
Output
Dog barks
Cat meows
Dog barks
Cat meows
Animal speaks

Rules of overriding (what beginners should remember)

  • Signatures must match: same method name and parameter list.
  • Use @Override to catch mistakes early.
  • Return types can be covariant (subtype of the original return type).
  • Access level cannot be reduced (e.g., public cannot become private).
  • final instance methods cannot be overridden.
  • private methods are not inherited and cannot be overridden.
  • static methods are not overridden; they are hidden and resolved at compile time.
Code
class Base {
    void greet() {
        System.out.println("Base.greet (instance)");
    }
    static void who() {
        System.out.println("Base.who (static)");
    }
}

class Sub extends Base {
    class="cd-annotation">@Override
    void greet() {
        System.out.println("Sub.greet (instance)");
    }
    static void who() {
        System.out.println("Sub.who (static)");
    }
}

class="cd-keyword cd-access">public class StaticVsOverrideDemo {
    class="cd-keyword cd-access">public static void main(String[] args) {
        Base ref = new Sub();

        // Dynamic dispatch for instance methods:
        ref.greet(); // Sub.greet (instance)

        // Static methods are resolved by reference type or class name:
        Base.who(); // Base.who (static)
        Sub.who();  // Sub.who (static)

        // The following is allowed but not recommended; it calls Base.who()
        ref.who();  // Base.who (static)
    }
}
Output
Sub.greet (instance)
Base.who (static)
Sub.who (static)
Base.who (static)

Method overloading vs overriding (quick comparison)

Aspect Compile-time polymorphism (Overloading) Runtime polymorphism (Overriding)
Decision time At compile time (by the compiler) At runtime (by the JVM via dynamic dispatch)
Where it happens Within the same class Between superclass and subclass
Signature rule Same name, different parameter list Same name and parameter list
Return type Does not distinguish overloads Can be covariant (subtype of original)
Static methods Can be overloaded Not applicable (static methods are hidden, not overridden)
Relative strengths: compile-time vs runtime polymorphism
Bars are qualitative to help students compare trade-offs.
Flexibility to swap behavior without changing call sites

Overloading: lower
Overriding: higher

Early error detection by the compiler

Overloading: higher
Overriding: medium

Risk of ambiguity/confusion at call site

Overloading: higher
Overriding: lower

Dispatch overhead at runtime

Overloading: minimal
Overriding: small


Overloading (compile-time)

Overriding (runtime)

When should a beginner use polymorphism in Java and why?

  • When multiple classes share behavior but differ in details (e.g., Animal subclasses speaking differently).
  • When you want to code to an interface and swap implementations easily (e.g., List can be ArrayList or LinkedList).
  • When you need cleaner, testable design: pass abstractions, not concrete classes.
  • Use overloading for convenience APIs; use overriding for flexible, extensible behavior.
Beginner checklist: choosing overloading vs overriding (Java polymorphism compile-time vs runtime examples)
  • If you changed the parameter list, you created an overload (compile-time).
  • If you kept the same signature in a subclass and changed the body, you made an override (runtime).
  • Always add @Override in subclasses to catch mistakes early.
  • Do not expect return type alone to differentiate overloads—Java ignores it for resolution.
  • Call static methods via the class name, not an object reference (no dynamic dispatch for statics).
  • For ambiguous overloads (e.g., test(Integer) vs test(Long)), cast the argument explicitly.
  • When overriding, you cannot reduce visibility and you should not throw broader checked exceptions than the superclass method.
  • Program to interfaces for runtime polymorphism: e.g., declare List<String> list = new ArrayList<>();

Modern Java tip: safer runtime polymorphism with sealed classes and pattern matching (Java 21+)

Java 17 introduced sealed classes and interfaces, and Java 21 added pattern matching for switch. Together, they make subtype polymorphism more explicit and safe for students. You restrict which subclasses are allowed, and you can switch over them exhaustively.

Code
sealed interface Shape permits Circle, Rectangle, Square {}

record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Square(double side) implements Shape {}

class="cd-keyword cd-access">public class ShapeDemo {
    static double area(Shape s) {
        return switch (s) {
            case Circle c    -> Math.PI * c.radius() * c.radius();
            case Rectangle r -> r.width() * r.height();
            case Square sq   -> sq.side() * sq.side();
        };
    }

    class="cd-keyword cd-access">public static void main(String[] args) {
        Shape[] shapes = {
            new Circle(2),
            new Rectangle(3, 4),
            new Square(5)
        };

        for (Shape s : shapes) {
            System.out.println(s.getClass().getSimpleName() + " area = " + area(s));
        }
    }
}
Output
Circle area = 12.566370614359172
Rectangle area = 12.0
Square area = 25.0

This approach avoids long if (s instanceof ...) chains and ensures you handle all permitted subtypes. It’s available in Java 21 and later.

Common mistakes with polymorphism in Java for learners

  • Thinking overloading is runtime polymorphism. It is compile-time.
  • Forgetting that return type alone cannot distinguish overloaded methods.
  • Calling static methods via an object reference and expecting dynamic dispatch. Static methods are resolved at compile time by the reference type.
  • Trying to override private or final methods. Private methods aren’t inherited; final methods can’t be overridden.
  • Changing method parameters when attempting to override, which actually creates an overload instead of an override. Use @Override to catch this.
  • Narrowing access modifier in subclass (e.g., public to protected). That’s not allowed.
  • Forgetting that constructors aren’t inherited and cannot be overridden—only overloaded.

Practice ideas for students

  • Write a Calculator with overloaded sum methods: sum(int, int), sum(double, double), sum(int, int, int).
  • Create a Vehicle base class and override a move() method in Car, Bike, and Bus. Store them in a Vehicle[] and call move().
  • Model shapes with a sealed interface and use a pattern matching switch to compute perimeters.

FAQ: Java polymorphism tutorial for students

What is polymorphism in Java in simple words?

It means the same method name can do different things depending on input types (overloading) or the real object type (overriding). It helps write flexible, reusable code.

How is compile-time polymorphism different from runtime polymorphism in Java?

Compile-time polymorphism (overloading) is decided by the compiler using method signatures. Runtime polymorphism (overriding) is decided when the program runs, based on the object’s actual type.

How do method overloading and overriding show polymorphism in Java?

  • Overloading: same method name, different parameters; the compiler chooses the best match.
  • Overriding: subclass replaces a superclass method; the JVM calls the subclass’s method at runtime through dynamic dispatch.

Can you give a basic example of runtime polymorphism using inheritance?

Yes: Animal a = new Dog(); a.speak(); will call Dog’s speak() at runtime, even though the reference type is Animal. See the “Runtime Polymorphism” example above.

When should a beginner use polymorphism in Java and why?

Use it when multiple types share a common behavior but differ in implementation. It makes your code easier to extend and test—just add a new subclass or implementation without changing existing logic.

Sources / Further reading

Keep exploring Java OOP, collections, exceptions, and more in our Java section: https://coddesire.com/java/

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted