Skip to content
Java core 3 min read

Polymorphism

Polymorphism—“many forms”—lets a single interface drive different behaviors depending on context. In Java it appears in two flavors: compile-time polymorphism through method overloading, and runtime polymorphism through method overriding. Together they let you write code against abstractions while concrete types supply the specifics.

Compile-Time Polymorphism (Overloading)

Overloading defines multiple methods with the same name but different parameter lists. The compiler picks the best match at compile time based on the argument types.

public class Printer {
    public void print(int x)    { System.out.println("int: " + x); }
    public void print(String s) { System.out.println("str: " + s); }
    public void print(double d) { System.out.println("dbl: " + d); }
}
Printer p = new Printer();
p.print(42);
p.print("hi");

Output:

int: 42
str: hi

Overloading resolution considers the declared types, never runtime values, which is why it is “static.”

Runtime Polymorphism (Overriding)

Overriding replaces an inherited method’s body in a subclass. The JVM chooses which version to run based on the object’s actual runtime type.

abstract class Shape { abstract double area(); }

class Circle extends Shape {
    private final double r;
    Circle(double r) { this.r = r; }
    @Override double area() { return Math.PI * r * r; }
}

class Square extends Shape {
    private final double s;
    Square(double s) { this.s = s; }
    @Override double area() { return s * s; }
}
Shape[] shapes = { new Circle(1), new Square(2) };
for (Shape shape : shapes) {
    System.out.printf("%.2f%n", shape.area());
}

Output:

3.14
4.00

Dynamic Dispatch

The mechanism that selects the runtime implementation is dynamic dispatch (or virtual method invocation). Each object carries a pointer to its class’s method table; calling shape.area() looks up the entry for the object’s real class, not the Shape reference type. This is what makes the loop above print two different values from one call site.

Note: Fields and static methods are not polymorphic. They resolve by declared type. Only instance methods are dynamically dispatched.

Upcasting and Downcasting

Upcasting—treating a subclass as its superclass—is implicit and always safe:

Shape s = new Circle(2); // upcast

Downcasting—narrowing back to a subclass—is explicit and checked at runtime; an invalid cast throws ClassCastException:

Circle c = (Circle) s; // downcast, valid here

Warning: Always guard a downcast. A blind cast that fails crashes with ClassCastException at runtime.

instanceof and Pattern Matching

The instanceof operator tests an object’s runtime type. Modern Java (16+) adds pattern matching for instanceof, which tests and binds in one step—no separate cast needed.

Object o = "devcraftly";

// Classic
if (o instanceof String) {
    String str = (String) o;
    System.out.println(str.length());
}

// Pattern matching (Java 16+)
if (o instanceof String str) {
    System.out.println(str.length());
}

Output:

10
10

Pattern matching also powers switch expressions (Java 21+), letting you dispatch cleanly over a sealed type hierarchy:

String describe(Shape shape) {
    return switch (shape) {
        case Circle c -> "circle r-area " + c.area();
        case Square sq -> "square " + sq.area();
        default -> "unknown";
    };
}

Best Practices

  • Prefer overriding to type-checking chains; let dynamic dispatch do the branching.
  • Reserve overloading for genuinely interchangeable parameter types; avoid confusingly similar overloads.
  • Program to abstractions (Shape), not concrete classes, to maximize substitutability.
  • Use pattern matching instanceof/switch to eliminate verbose casts.
  • Keep downcasts rare and always guarded; frequent downcasting signals a design smell.

Interview Questions

Q: What is the difference between overloading and overriding? A: Overloading is compile-time polymorphism—same name, different parameters, resolved by declared types. Overriding is runtime polymorphism—same signature in a subclass, resolved by the object’s actual type via dynamic dispatch.

Q: Are static methods polymorphic? A: No. Static methods are bound at compile time by declared type (method hiding), so they do not participate in dynamic dispatch.

Q: What happens if you downcast to an incompatible type? A: The JVM throws a ClassCastException at runtime. Guard downcasts with instanceof (ideally pattern matching) to avoid it.

Last updated June 1, 2026
Was this helpful?