Java Polymorphism
Polymorphism — Greek for “many forms” — is the ability of one reference to refer to objects of different actual types. In Java, the most common form is runtime polymorphism through method overriding: a parent-type reference calls the version of the method that matches the actual object's class.
This tutorial explains virtual method dispatch, the difference between static and dynamic binding, polymorphism through interfaces, and the patterns it enables (strategy, template method, factory). Polymorphism is the single most powerful design tool in object-oriented Java — master it.
1. Virtual Method Dispatch
When you call a method on a parent-type reference, the JVM looks up the actual class of the object at runtime and calls that class's override:
public class Animal {
public String sound() { return "?"; }
}
public class Dog extends Animal {
@Override public String sound() { return "Woof"; }
}
public class Cat extends Animal {
@Override public String sound() { return "Meow"; }
}
class=class="tok-str">"tok-cmt">// polymorphism in action
Animal[] zoo = { new Dog(), new Cat(), new Dog() };
for (Animal a : zoo) {
System.out.println(a.sound()); class=class="tok-str">"tok-cmt">// Woof, Meow, Woof
}
class=class="tok-str">"tok-cmt">// parent-type reference, child object
Animal a = new Cat();
System.out.println(a.sound()); class=class="tok-str">"tok-cmt">// Meow - the actual class decides
This is the essence of “program to an interface, not an implementation”. The caller does not know or care which concrete subclass it is talking to.
2. Static vs Dynamic Binding
Method calls are resolved either at compile time (static binding) or at runtime (dynamic / virtual binding):
private,static, andfinalmethods use static binding.- All other instance methods use dynamic binding — the actual class is consulted at runtime.
public class Base {
public void dynamic() { System.out.println("Base.dynamic"); }
public static void staticMethod() { System.out.println("Base.static"); }
private void priv() { System.out.println("Base.priv"); }
}
public class Sub extends Base {
@Override public void dynamic() { System.out.println("Sub.dynamic"); }
public static void staticMethod() { System.out.println("Sub.static"); }
public static void main(String[] args) {
Base b = new Sub();
b.dynamic(); class=class="tok-str">"tok-cmt">// "Sub.dynamic" - dynamic binding
b.staticMethod(); class=class="tok-str">"tok-cmt">// "Base.static" - static binding (looks at type)
class=class="tok-str">"tok-cmt">// Hiding a static method is not overriding.
}
}
This is why private methods are not overridable (no point — the call site already knows the type) and final methods are not (the compiler can inline them).
3. Polymorphism via Interfaces
Interfaces are the most powerful way to achieve polymorphism. A reference of an interface type can point to any object whose class implements it:
public interface Shape {
double area();
}
public record Circle(double r) implements Shape {
public double area() { return Math.PI * r * r; }
}
public record Square(double s) implements Shape {
public double area() { return s * s; }
}
public record Triangle(double b, double h) implements Shape {
public double area() { return b * h / class="tok-num">2; }
}
class=class="tok-str">"tok-cmt">// a list of shapes - heterogeneous, all accessed through Shape
List<Shape> shapes = List.of(
new Circle(class="tok-num">2.0),
new Square(class="tok-num">3.0),
new Triangle(class="tok-num">4.0, class="tok-num">5.0)
);
double totalArea = class="tok-num">0;
for (Shape s : shapes) {
totalArea += s.area(); class=class="tok-str">"tok-cmt">// dispatched to the right implementation
}
The list can hold heterogeneous implementations, and the loop treats them all uniformly through the interface. Adding a new Shape implementation requires no change to the loop — this is the “open for extension, closed for modification” principle in action.
4. The Strategy Pattern
Polymorphism lets you swap behaviour at runtime by injecting different implementations:
public interface DiscountStrategy {
double apply(double total);
}
public class NoDiscount implements DiscountStrategy {
public double apply(double total) { return total; }
}
public class PercentageDiscount implements DiscountStrategy {
private final double pct;
public PercentageDiscount(double pct) { this.pct = pct; }
public double apply(double total) { return total * (class="tok-num">1 - pct / class="tok-num">100); }
}
public class FixedDiscount implements DiscountStrategy {
private final double amount;
public FixedDiscount(double amount) { this.amount = amount; }
public double apply(double total) { return Math.max(class="tok-num">0, total - amount); }
}
public class Checkout {
private DiscountStrategy strategy = new NoDiscount();
public void setStrategy(DiscountStrategy s) { this.strategy = s; }
public double total(double cartTotal) { return strategy.apply(cartTotal); }
}
Checkout c = new Checkout();
c.setStrategy(new PercentageDiscount(class="tok-num">10));
System.out.println(c.total(class="tok-num">100.0)); class=class="tok-str">"tok-cmt">// class="tok-num">90.0
c.setStrategy(new FixedDiscount(class="tok-num">15));
System.out.println(c.total(class="tok-num">100.0)); class=class="tok-str">"tok-cmt">// class="tok-num">85.0
Modern Java favours lambdas over anonymous strategy classes when the interface is functional (a single abstract method), as we will see in the Lambda Expressions tutorial.
5. Template Method Pattern
An abstract parent defines the algorithm skeleton and lets subclasses fill in the steps:
public abstract class DataProcessor {
class=class="tok-str">"tok-cmt">// template method - final so subclasses cannot change the flow
public final void process(String input) {
String cleaned = clean(input);
String parsed = parse(cleaned);
save(parsed);
}
protected abstract String clean(String raw);
protected abstract String parse(String cleaned);
protected void save(String data) { class=class="tok-str">"tok-cmt">/* default no-op */ }
}
public class CsvProcessor extends DataProcessor {
@Override protected String clean(String raw) { return raw.trim(); }
@Override protected String parse(String cleaned) { return cleaned.toUpperCase(); }
}
This is a classic OO pattern. Override only the bits that vary; the parent controls the flow.
6. Type Checking and Casting
Polymorphism lets you call only methods declared on the reference's type. When you need a method on the actual type, you must cast — but check first with instanceof to avoid ClassCastException:
Object obj = "Hello";
class=class="tok-str">"tok-cmt">// classic - test then cast
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
class=class="tok-str">"tok-cmt">// pattern (Java class="tok-num">16+) - test and bind in one
if (obj instanceof String s) {
System.out.println(s.length()); class=class="tok-str">"tok-cmt">// s is in scope
} else if (obj instanceof Integer i) {
System.out.println(i * class="tok-num">2);
}
class=class="tok-str">"tok-cmt">// switch pattern (Java class="tok-num">21+)
String desc = switch (obj) {
case String s -> "string of length " + s.length();
case Integer i -> "int " + i;
case null -> "null";
default -> "other";
};
Since Java 16, instanceof can bind a pattern variable, removing the boilerplate cast. The need to cast should be rare — if your code is full of instanceof checks, you are likely missing a polymorphic method.
7. Pitfalls
If a parent constructor calls a method that the subclass overrides, the subclass version runs — before the subclass constructor body. The subclass fields are not yet initialised. This is a famous footgun; avoid calling non-final, non-private methods from constructors.
@OverrideIf you intend to override but spell the method name wrong, you silently add a new method. Always annotate with @Override so the compiler catches the typo.
Exercises
- Define an interface
PaymentMethodwith methodpay(double amount). Implement it withCreditCard,PayPal,BankTransfer. Loop through a list of them. - Build a
Loggerhierarchy with a template methodlog(message)that callsformat(message)andemit(line). - Refactor an
if/elsechain ofinstanceofchecks into a polymorphic method. - Use a pattern
instanceofto safely extract a value from anObjectthat may be aStringor anInteger.