Polymorphism in Java β€” Method Overriding Explained

Polymorphism means "one name, many forms". In Java it appears as two separate mechanisms: method overriding (runtime polymorphism, dispatched by the object's actual type) and method overloading (compile-time, dispatched by the argument types).

Runtime polymorphism β€” overriding

abstract class Shape {
    public abstract double area();
}
class Circle extends Shape {
    private final double r;
    public Circle(double r) { this.r = r; }
    public double area() { return Math.PI * r * r; }
}
class Square extends Shape {
    private final double s;
    public Square(double s) { this.s = s; }
    public double area() { return s * s; }
}

List<Shape> shapes = List.of(new Circle(3), new Square(4));
for (Shape s : shapes) {
    System.out.println(s.area());    // calls Circle.area or Square.area
}

The compiler sees Shape s. The JVM, at the call site, looks at the real object and dispatches to its area(). This is dynamic dispatch.

Compile-time polymorphism β€” overloading

class Logger {
    public void log(String msg) { ... }
    public void log(String msg, Throwable t) { ... }
    public void log(int code, String msg)   { ... }
}
// The compiler picks an overload at compile time based on the argument types.

Why it's useful

You can write code against a contract (an interface or abstract class) and the actual implementation varies at runtime:

public interface PaymentProvider {
    void charge(BigDecimal amount);
}

public class OrderService {
    private final PaymentProvider provider;   // Stripe? Paypal? Mock? Doesn't matter.
    public void payOrder(Order o) {
        provider.charge(o.total());
    }
}

Covariant returns

class Shape { public Shape copy() { ... } }
class Circle extends Shape {
    @Override
    public Circle copy() { return new Circle(...); }   // Circle is more specific β€” legal
}

The static trap

class A { public static String who() { return "A"; } }
class B extends A { public static String who() { return "B"; } }

A a = new B();
a.who();                  // "A" β€” static methods are NOT overridden, only hidden
// The call resolves by compile-time type (A), not runtime type.

Common mistakes

  • Expecting static to be polymorphic β€” it isn't. Use instance methods for polymorphic behaviour.
  • Calling overridable methods from a constructor β€” subclass override runs on a half-constructed object.
  • Overloading + autoboxing β€” log(1) might match log(int) or log(Integer). Be explicit if both exist.

Related

Pillar: OOP in Java. Siblings: inheritance, method overloading, interfaces.