Polymorphism
Polymorphism lets one reference type represent many actual behaviors at runtime — the payoff that inheritance and overriding exist to enable.
Polymorphism means "many forms": the same reference type, or the same method call written once, can behave differently depending on the actual object involved. In Java, the most important form is subtype polymorphism — a superclass or interface reference can point to any compatible subtype object, and calling an overridden method through that reference runs the specific subtype's version, decided at runtime.
Subtype Polymorphism
Because a subclass is-a superclass, a variable declared with the superclass (or an implemented interface) as its type can hold a reference to any object of that subclass. This is the foundation polymorphism is built on.
Both a1 and a2 are declared as Animal, but each actually refers to a different concrete subtype underneath.
class Animal {
void speak() { System.out.println("..."); }
}
class Dog extends Animal {
@Override void speak() { System.out.println("Woof"); }
}
class Cat extends Animal {
@Override void speak() { System.out.println("Meow"); }
}
Animal a1 = new Dog(); // Animal-typed reference, Dog object
Animal a2 = new Cat(); // Animal-typed reference, Cat objectUpcasting
Assigning a subtype object to a supertype-typed reference, as shown above, is called upcasting. It happens implicitly — no cast operator is needed — because it is always safe: a Dog can always be treated as an Animal.
Upcasting changes the compile-time type of how the object is referred to; it does not change the object itself or what it actually is.
The reverse — downcasting a supertype reference back to a subtype — requires an explicit cast and can fail at runtime with a ClassCastException if the object is not actually of that subtype; it is a separate topic from the upcasting polymorphism relies on.
Dog d = new Dog();
Animal a = d; // implicit upcast, no cast syntax required
a.speak(); // still runs Dog's speak() -- see Dynamic Method Dispatch belowDynamic Method Dispatch
When an overridden instance method is called through a reference, Java decides which class's version to run based on the object's actual runtime type — not the reference's declared (compile-time) type. This is dynamic method dispatch, and it is what makes polymorphism useful.
The compiler checks at compile time that Animal has a speak() method — this determines what is legal to write, based on the reference type.
At runtime, the JVM looks at the actual object a refers to (a Dog) to decide which speak() implementation to execute.
Dog's overriding speak() runs, producing "Woof".
Animal a = new Dog();
a.speak(); // prints "Woof" -- Dog's version runs, even though "a" is typed as AnimalRuntime Behavior in Practice
Dynamic dispatch means a single piece of code, written once against a supertype, automatically behaves correctly for every subtype — including ones written after that code, as long as they follow the same contract.
announceAll never checks what kind of Animal each element is — it just calls speak() and trusts dynamic dispatch to run the right implementation for each one.
class AnimalShelter {
void announceAll(Animal[] animals) {
for (Animal a : animals) {
a.speak(); // one line, many possible behaviors
}
}
}
Animal[] animals = { new Dog(), new Cat(), new Dog() };
new AnimalShelter().announceAll(animals);
// Output:
// Woof
// Meow
// WoofPolymorphism Relies on Overriding
Polymorphism and overriding are tightly linked but distinct: overriding is the mechanism (a subclass supplying its own implementation of an inherited method, covered fully in the Method Overriding topic); polymorphism is the resulting behavior (a call resolving to different implementations depending on the object's actual type). Without overriding, there would be nothing for dynamic dispatch to choose between.
Compile-Time Overloading versus Runtime Overriding
It is important to distinguish two mechanisms that can both be loosely called "polymorphism." Method overloading (multiple methods, same name, different parameter lists) is resolved entirely at compile time, based on the declared types of the arguments at the call site. Method overriding is resolved at runtime, based on the object's actual type. These are sometimes called compile-time (static) polymorphism and runtime (dynamic) polymorphism, respectively.
Both calls print the same underlying String, but overload resolution picks a different print(...) method based purely on each variable's compile-time type — this is decided before the program ever runs, unlike overriding.
class Printer {
void print(String s) { System.out.println("string: " + s); }
void print(Object o) { System.out.println("object: " + o); }
}
Printer p = new Printer();
String value = "hi";
Object asObject = value; // same object, different declared/compile-time type
p.print(value); // "string: hi" -- chosen at compile time from value's declared type (String)
p.print(asObject); // "object: hi" -- chosen at compile time from asObject's declared type (Object)Polymorphic References and Collections
Polymorphism is what allows a single collection or array typed by a common supertype (or interface) to hold a mix of different concrete subtypes, and be processed uniformly.
The List<Shape> holds two genuinely different classes, but the loop body needs no type checks — it trusts polymorphism to route area() correctly each time.
import java.util.List;
interface Shape {
double area();
}
class Circle implements Shape {
double radius;
Circle(double r) { radius = r; }
public double area() { return Math.PI * radius * radius; }
}
class Square implements Shape {
double side;
Square(double s) { side = s; }
public double area() { return side * side; }
}
List<Shape> shapes = List.of(new Circle(2), new Square(3));
double total = 0;
for (Shape s : shapes) {
total += s.area(); // each call dispatches to the correct implementation
}
System.out.println(total);Practical Usage
Polymorphism is the basis for extensible designs: plugin-style architectures, strategy patterns, and framework callback methods all rely on code written against a supertype or interface automatically working correctly with new subtypes added later, without modification.
Complete Example
Payroll never checks whether an Employee is salaried or hourly — it calls monthlyPay() once per element and relies entirely on dynamic dispatch to compute the right amount for each.
abstract class Employee {
protected String name;
Employee(String name) { this.name = name; }
abstract double monthlyPay();
}
class SalariedEmployee extends Employee {
private double monthlySalary;
SalariedEmployee(String name, double monthlySalary) {
super(name);
this.monthlySalary = monthlySalary;
}
@Override double monthlyPay() { return monthlySalary; }
}
class HourlyEmployee extends Employee {
private double hourlyRate;
private double hoursThisMonth;
HourlyEmployee(String name, double hourlyRate, double hoursThisMonth) {
super(name);
this.hourlyRate = hourlyRate;
this.hoursThisMonth = hoursThisMonth;
}
@Override double monthlyPay() { return hourlyRate * hoursThisMonth; }
}
public class Payroll {
public static void main(String[] args) {
Employee[] staff = {
new SalariedEmployee("Ada", 6000),
new HourlyEmployee("Sam", 25, 160)
};
double total = 0;
for (Employee e : staff) {
total += e.monthlyPay(); // polymorphic call
}
System.out.println("Total payroll: " + total);
}
}Mental Model
Picture a reference variable's declared type as a "window" through which code can only see the methods that type promises — but when a method call actually runs, the JVM looks past the window at the real object behind it and runs that object's own version. The window controls what you're allowed to call; the actual object controls what happens when you call it.
Quick Reference
Term Resolved when Based on
-------------------------- -------------------- -------------------------------
Method overloading compile time declared (static) types of arguments
Method overriding runtime actual (dynamic) type of the object
Upcasting compile time always implicit/safe, subtype -> supertype
Downcasting runtime (checked) explicit cast; can throw ClassCastExceptionKey Takeaways
Subtype polymorphism lets a supertype or interface reference point to any compatible subtype object.
Upcasting (subtype to supertype) is implicit and always safe; it changes only the reference's compile-time type.
Dynamic method dispatch resolves an overridden method call using the object's actual runtime type, not the reference's declared type.
Overloading is resolved at compile time from argument types; overriding is resolved at runtime from the object's type.
Polymorphism lets code written once against a supertype work correctly with new subtypes added later.
Animal a = new Dog(); // developer assumes "a.speak()" depends on how "a" is declared if (a instanceof Animal) { System.out.println("generic animal sound"); // wrong assumption }
Animal a = new Dog(); a.speak(); // runs Dog's overridden version -- runtime type governs, not the declared reference type
The declared (compile-time) type of a reference only controls which methods are callable on it — which implementation actually runs for an overridden method is decided by the object's real, runtime type.
class Printer { void print(int x) { System.out.println("int: " + x); } void print(double x) { System.out.println("double: " + x); } } Printer p = new Printer(); Object value = 5; // declared as Object p.print(value); // compile error: no print(Object) overload exists class Printer { void print(int x) { System.out.println("int: " + x); } void print(double x) { System.out.println("double: " + x); } void print(Object x) { System.out.println("object: " + x); } }
Overload resolution is based on the argument's compile-time (declared) type, not its runtime value — a variable declared as Object will only match a print(Object) overload, even if it happens to hold an Integer at runtime, unless that overload exists.
class Shape { double area() { return 0; } } class Circle extends Shape { double area(double radius) { return Math.PI * radius * radius; } // different signature! } Shape s = new Circle(); System.out.println(s.area()); // prints 0 -- NOT the Circle-specific logic class Shape { double area() { return 0; } } class Circle extends Shape { private double radius = 2; @Override double area() { return Math.PI * radius * radius; } // matches signature -- true override }
Circle.area(double) has a different signature than Shape.area(), so it overloads rather than overrides — polymorphic dispatch through a Shape reference never reaches it; @Override would have caught this immediately.
Object[] items = { "text", 42, 3.14 }; for (Object o : items) { System.out.println(o.length()); // compile error: Object has no length() method }
Object[] items = { "text", 42, 3.14 }; for (Object o : items) { if (o instanceof String s) { System.out.println(s.length()); } else { System.out.println(o); } }
Polymorphic dispatch only works for methods that actually exist on the reference's declared type; calling a subtype-specific method on a common supertype reference requires a check (or cast) first, not blind reliance on runtime dispatch.
What is the difference between upcasting and downcasting?
Upcasting assigns a subtype object to a supertype (or interface) reference, is always implicit, and never fails. Downcasting converts a supertype reference back to a subtype, requires an explicit cast, and can throw a ClassCastException at runtime if the object is not actually of that subtype.
When you call an overridden method through a supertype reference, what determines which implementation runs?
The object's actual runtime type — not the reference's declared/compile-time type. This is dynamic method dispatch.
Is method overloading resolved at compile time or runtime?
Compile time — the compiler picks which overloaded method to call based on the declared (static) types of the arguments at the call site, before the program runs.
Why does polymorphism let new subclasses "just work" with existing code?
Because code written against the supertype or interface calls methods through dynamic dispatch; as long as a new subclass correctly implements that contract, the existing code invokes it correctly without any modification, since it never needed to know the concrete type in advance.
A List<Shape> holds a Circle and a Square. Why can a loop call shape.area() on every element without checking each one's concrete type?
Because area() is declared on the Shape type (an interface or superclass) and each concrete class provides its own overriding implementation; dynamic dispatch automatically routes each call to the correct implementation based on the actual object, so no manual type check is needed.
Why is calling a variable typed as Object.length() a compile error, even if it happens to hold a String at runtime?
Method calls are checked against the reference's declared (compile-time) type. Object has no length() method, so the call is illegal regardless of what the variable actually references at runtime — polymorphic dispatch never gets a chance to run because the code doesn't even compile.
Interview-style: why are overloading and overriding both sometimes described as "polymorphism," despite working very differently?
Both let the same method name behave differently depending on context — overloading by argument types (resolved statically, at compile time), overriding by the object's actual type (resolved dynamically, at runtime). They're often distinguished as compile-time/static polymorphism and runtime/dynamic polymorphism respectively.
Write a Shape interface with area(), and Circle/Rectangle implementations. Then write a method totalArea(Shape[] shapes) that sums the areas polymorphically.
interface Shape { double area(); } class Circle implements Shape { private final double radius; Circle(double radius) { this.radius = radius; } public double area() { return Math.PI * radius * radius; } } class Rectangle implements Shape { private final double width, height; Rectangle(double width, double height) { this.width = width; this.height = height; } public double area() { return width * height; } } public class Geometry { static double totalArea(Shape[] shapes) { double total = 0; for (Shape s: shapes) { total += s.area(); // dynamic dispatch picks the right area() each time } return total; } } totalArea never inspects which concrete class each Shape is; dynamic method dispatch resolves s.area() to Circle's or Rectangle's implementation automatically based on each element's actual runtime type.
What does each call print, and why do they differ despite both arguments holding the same String value?
b.show(x) prints "Object version" and b.show("hello") prints "String version". Overload resolution uses each argument's compile-time (declared) type: x is declared as Object even though it references a String at runtime, while the literal "hello" is directly of type String — this decision happens at compile time and ignores the actual runtime value.
Write an Animal supertype with speak(), and Dog/Cat subclasses. Write a method that takes an Animal[] and calls speak() on each, demonstrating dynamic dispatch, and show what happens if one element is upcast explicitly.
class Animal { void speak() { System.out.println("..."); } } class Dog extends Animal { @Override void speak() { System.out.println("Woof"); } } class Cat extends Animal { @Override void speak() { System.out.println("Meow"); } } public class Zoo { static void announce(Animal[] animals) { for (Animal a : animals) { a.speak(); } } public static void main(String[] args) { Dog d = new Dog(); Animal upcast = d; // explicit variable, implicit upcast Animal[] animals = { upcast, new Cat() }; announce(animals); // prints "Woof" then "Meow" } }
Assigning d to the Animal-typed variable upcast changes only the compile-time type used to refer to the object; the object is still a Dog underneath, so a.speak() inside announce() still dispatches to Dog's overriding implementation.
Car does not override drive(). What gets printed, and why?
Prints "driving". Car inherits Vehicle's drive() unchanged, so calling v.drive() runs the only version that exists — Vehicle's — regardless of the fact that v's actual runtime type is Car. Polymorphism only changes behavior when a subclass actually overrides the method being called.
class Base {
void show(Object o) { System.out.println("Object version"); }
void show(String s) { System.out.println("String version"); }
}
public class Main {
public static void main(String[] args) {
Base b = new Base();
Object x = "hello";
b.show(x);
b.show("hello");
}
}class Vehicle {
void drive() { System.out.println("driving"); }
}
class Car extends Vehicle { }
public class Main {
public static void main(String[] args) {
Vehicle v = new Car();
v.drive();
}
}