00:00

Polymorphism in Java – Compile-Time and Runtime Polymorphism

Polymorphism, from the Greek for "many forms", is the ability of one name to stand for different behaviour depending on context. In Java, the same method call can run different code depending on the arguments (compile-time polymorphism) or on the actual object it is called on (runtime polymorphism).

Polymorphism is what lets you write code against a general type such as Payment or List and have it work correctly with every specific type. This tutorial explains both kinds with runnable examples, along with upcasting, downcasting and the things that are not polymorphic.

1. Types of Polymorphism in Java

AspectCompile-time (static)Runtime (dynamic)
Achieved byMethod overloadingMethod overriding
Method chosenBy the compiler, from argument typesBy the JVM, from the actual object's class
Also calledStatic binding, early bindingDynamic method dispatch, late binding
Requires inheritanceNoYes (class or interface)

2. Compile-Time Polymorphism: Overloading

Several methods share a name but differ in their parameter lists. The compiler chooses one based on the arguments:

CompileTimePolymorphism.java
public class CompileTimePolymorphism {
    // Same name, different parameter lists: the compiler picks the method
    static int area(int side)                 { return side * side; }
    static int area(int length, int width)    { return length * width; }
    static double area(double radius)         { return Math.PI * radius * radius; }

    public static void main(String[] args) {
        System.out.println("Square:    " + area(5));
        System.out.println("Rectangle: " + area(4, 6));
        System.out.printf("Circle:    %.2f%n", area(2.0));
        // println itself is overloaded: println(int), println(String), println(Object), ...
    }
}
Output
Square:    25
Rectangle: 24
Circle:    12.57

See Method Overloading for the full rules.

3. Runtime Polymorphism: Overriding and Dynamic Dispatch

A subclass overrides a method of its parent. When the method is called through a parent-type reference, the JVM runs the version belonging to the actual object:

RuntimePolymorphism.java
import java.util.List;

public class RuntimePolymorphism {
    static class Payment {
        double fee(double amount) { return 0; }
        String type() { return "Generic"; }
    }
    static class CardPayment extends Payment {
        @Override double fee(double amount) { return amount * 0.02; }
        @Override String type() { return "Card"; }
    }
    static class UpiPayment extends Payment {
        @Override double fee(double amount) { return 0; }
        @Override String type() { return "UPI"; }
    }
    static class WalletPayment extends Payment {
        @Override double fee(double amount) { return Math.min(10, amount * 0.01); }
        @Override String type() { return "Wallet"; }
    }

    public static void main(String[] args) {
        // Reference type: Payment. Object type: decided at run time.
        List<Payment> payments = List.of(new CardPayment(), new UpiPayment(), new WalletPayment());
        for (Payment p : payments) {
            System.out.printf("%-6s fee on 2000 = %.2f%n", p.type(), p.fee(2000));
        }
    }
}
Output
Card   fee on 2000 = 40.00
UPI    fee on 2000 = 0.00
Wallet fee on 2000 = 10.00

The loop knows nothing about cards or wallets. Adding a new NetBankingPayment class requires no change to it, which is the open/closed principle in action. See Method Overriding.

4. Upcasting and Downcasting

Upcasting (child object to parent reference) is automatic and safe. The reference type limits which methods you can call; the object type decides which version runs. Downcasting (back to the child type) needs an explicit cast and is safe only if the object really is of that type:

UpcastingDowncasting.java
public class UpcastingDowncasting {
    static class Animal {
        void sound() { System.out.println("Some sound"); }
    }
    static class Dog extends Animal {
        @Override void sound() { System.out.println("Woof"); }
        void fetch() { System.out.println("Dog fetches the ball"); }
    }
    static class Cat extends Animal {
        @Override void sound() { System.out.println("Meow"); }
    }

    public static void main(String[] args) {
        Animal a = new Dog();          // upcasting: automatic and always safe
        a.sound();                     // Woof  (object type decides)
        // a.fetch();                  // compile error: Animal has no fetch()

        if (a instanceof Dog d) {      // safe downcast with pattern matching (Java 16+)
            d.fetch();
        }

        Animal c = new Cat();
        try {
            Dog wrong = (Dog) c;       // compiles, but fails at run time
        } catch (ClassCastException e) {
            System.out.println("ClassCastException: a Cat is not a Dog");
        }
    }
}
Output
Woof
Dog fetches the ball
ClassCastException: a Cat is not a Dog

Use instanceof (with pattern matching since Java 16) before downcasting. Frequent downcasting is often a sign that a method should be added to the parent type instead.

5. What Is Not Polymorphic

Only overridable instance methods are dispatched dynamically. Fields, static methods and private methods are resolved by the reference type at compile time:

FieldsNotPolymorphic.java
public class FieldsNotPolymorphic {
    static class Parent {
        String name = "Parent field";
        static String id() { return "Parent static"; }
        String describe() { return "Parent method"; }
    }
    static class Child extends Parent {
        String name = "Child field";                   // hides, does not override
        static String id() { return "Child static"; }  // hides, does not override
        @Override String describe() { return "Child method"; }
    }

    public static void main(String[] args) {
        Parent p = new Child();
        System.out.println(p.describe());   // instance method: overridden -> Child
        System.out.println(p.name);         // field: chosen by reference type -> Parent
        System.out.println(Parent.id());    // static: chosen at compile time
    }
}
Output
Child method
Parent field
Parent static
  • Fields are hidden, not overridden.
  • Static methods are hidden, not overridden.
  • Private and final methods cannot be overridden, so they are bound statically.
  • Constructors are not inherited and therefore not polymorphic.

6. Polymorphism with Interfaces and Sealed Types

Polymorphism works equally with interfaces: a List<String> variable may hold an ArrayList or a LinkedList. Modern Java adds another style: sealed types with pattern-matching switch, where the compiler checks that every subtype is handled:

SealedPolymorphism.java
public class SealedPolymorphism {
    sealed interface Shape permits Circle, Square, Rect { }
    record Circle(double r) implements Shape { }
    record Square(double side) implements Shape { }
    record Rect(double w, double h) implements Shape { }

    // Pattern matching switch: exhaustive because Shape is sealed (Java 21)
    static double area(Shape s) {
        return switch (s) {
            case Circle c -> Math.PI * c.r() * c.r();
            case Square q -> q.side() * q.side();
            case Rect r   -> r.w() * r.h();
        };
    }

    public static void main(String[] args) {
        Shape[] shapes = { new Circle(1), new Square(3), new Rect(2, 5) };
        for (Shape s : shapes) System.out.printf("%-25s area = %.2f%n", s, area(s));
    }
}
Output
Circle[r=1.0]             area = 3.14
Square[side=3.0]          area = 9.00
Rect[w=2.0, h=5.0]        area = 10.00

Overriding puts behaviour in each class; pattern matching puts it in one function. The first makes adding types easy, the second makes adding operations easy.

7. Benefits of Polymorphism

  • Extensibility: new subclasses work with existing code.
  • Simpler code: one loop or method handles many types; no long if-else chains on type.
  • Loose coupling: code depends on abstractions, not concrete classes.
  • Reusability: generic algorithms (sorting, collections) work with any compatible type.

8. Summary

  • Polymorphism means one interface, many implementations.
  • Compile-time polymorphism uses overloading; the compiler picks the method from argument types.
  • Runtime polymorphism uses overriding; the JVM picks the method from the object's actual class.
  • Upcasting is automatic; downcasting needs a cast and an instanceof check.
  • Fields, static, private and final methods are not dynamically dispatched.

Related Tutorials