Polymorphism
Polymorphism — from the Greek poly (many) + morphē (form) — is the ability of a single interface, method name, or reference to represent many different forms of behaviour. It is one of the four pillars of object-oriented programming, and it is what lets you write code that stays correct and readable even as your system grows.
What Is Polymorphism?
At its core, polymorphism means you can write code against a general type and have the right concrete behaviour execute automatically at the right time — without scattering if/else or switch chains everywhere.
Java gives you two distinct flavours:
| Flavour | Resolved at | Mechanism |
|---|---|---|
| Compile-time (static) | Compile time | Method overloading — same name, different signatures |
| Runtime (dynamic) | Runtime | Method overriding + reference upcasting |
Both feel like “calling one method name”, but the JVM figures out which version to run at very different stages.
A Quick Taste
class Shape {
double area() { return 0; }
}
class Circle extends Shape {
double radius;
Circle(double r) { this.radius = r; }
@Override
double area() { return Math.PI * radius * radius; }
}
class Rectangle extends Shape {
double width, height;
Rectangle(double w, double h) { this.width = w; this.height = h; }
@Override
double area() { return width * height; }
}
public class Demo {
public static void main(String[] args) {
Shape[] shapes = { new Circle(5), new Rectangle(4, 6) };
for (Shape s : shapes) {
System.out.printf("Area: %.2f%n", s.area()); // correct method chosen at runtime
}
}
}
Output:
Area: 78.54
Area: 24.00
The variable s is declared as Shape, but Java calls the right area() method for each actual object. That is runtime polymorphism in action. No instanceof checks, no if chains — the objects “know” what to do.
Compile-Time vs Runtime Polymorphism
Compile-Time Polymorphism (Method Overloading)
The compiler picks the right method by matching the number and types of arguments you supply. This resolution happens before the program even runs.
class Printer {
void print(int n) { System.out.println("int: " + n); }
void print(double d) { System.out.println("double: " + d); }
void print(String s) { System.out.println("String: " + s); }
}
Calling new Printer().print(42) resolves to print(int) at compile time. See Compile-Time Polymorphism for the full story.
Runtime Polymorphism (Method Overriding)
A subclass provides its own version of an inherited method. The JVM decides which version to call at runtime, based on the actual object type — not the declared reference type. This is sometimes called dynamic dispatch.
class Animal {
void sound() { System.out.println("..."); }
}
class Dog extends Animal {
@Override
void sound() { System.out.println("Woof"); }
}
class Cat extends Animal {
@Override
void sound() { System.out.println("Meow"); }
}
public class Main {
public static void main(String[] args) {
Animal a = new Dog(); // upcasting
a.sound(); // prints "Woof" — resolved at runtime
}
}
Output:
Woof
The reference type is Animal, but a actually points to a Dog object — so the JVM runs Dog.sound(). See Runtime Polymorphism for more detail.
Tip: Always add the
@Overrideannotation when you intend to override a method. If you mistype the method name, the compiler will catch it instead of silently creating an unrelated new method.
Upcasting and Downcasting
Upcasting assigns a subclass object to a superclass reference. It is always safe and happens implicitly.
Animal a = new Dog(); // implicit upcast — safe, always works
Downcasting goes the other way — back to the concrete type. It must be explicit and can throw a ClassCastException if the actual object is not of that type.
Animal a = new Dog();
Dog d = (Dog) a; // explicit downcast — safe here because a IS a Dog
d.sound(); // "Woof"
Warning: Downcasting to the wrong type throws
ClassCastExceptionat runtime. Always guard withinstanceofbefore casting an unknown reference.
if (a instanceof Dog dog) { // Java 16+ pattern matching
dog.sound();
}
See the instanceof Operator page for the pattern-matching form available since Java 16.
Under the Hood
How the JVM Selects the Right Method
Every class in Java gets a vtable (virtual method table) — an internal array of method pointers built by the JVM when the class is loaded. When you invoke an overridable method on an object reference, the JVM:
- Looks up the object’s actual class (stored in the object header).
- Finds that class’s vtable.
- Calls the method pointer at the correct slot.
This lookup adds a tiny indirection cost compared to a direct call, but the JIT compiler (see JIT Compilation & Bytecode) typically inlines monomorphic call sites — sites where only one concrete type ever appears — making the overhead effectively zero in hot code.
For a deep dive into vtables, see vtable & Dynamic Dispatch.
Static Binding vs Dynamic Binding
Not every method call goes through the vtable. The JVM uses static binding (resolved at compile time) for:
staticmethodsprivatemethodsfinalmethods- Constructors
Everything else uses dynamic binding (resolved at runtime). See Static & Dynamic Binding.
Bytecode Opcodes
The Java compiler emits different bytecode instructions depending on the call type:
| Instruction | Used for |
|---|---|
invokevirtual | Regular instance methods — uses vtable |
invokeinterface | Interface methods |
invokestatic | Static methods |
invokespecial | Constructors, private, super calls |
invokedynamic | Lambdas, method handles (Java 7+) |
Why Polymorphism Matters
Polymorphism is the mechanism behind the Open/Closed Principle — your code is open for extension (add a new Shape subclass) but closed for modification (the for loop that prints areas never changes). It enables:
- Plugin architectures — swap implementations without touching caller code.
- Collections of mixed types — a
List<Animal>can holdDog,Cat,Birdobjects. - Testing — pass a mock or stub object anywhere the real type is expected.
Note: Polymorphism works through Interfaces just as well as through class inheritance — and often more flexibly. An interface reference can point to any object that implements it, giving you runtime polymorphism without any inheritance relationship.
In This Section
- Compile-Time Polymorphism — How method overloading lets the compiler choose the right method signature before the program runs.
- Runtime Polymorphism — How overriding and upcasting let the JVM pick the right method at runtime based on the actual object type.
- Method Overloading — Define multiple methods with the same name but different parameter lists in the same class.
- Method Overriding — Provide a subclass-specific implementation of a method inherited from a parent class.
- Overloading vs Overriding — A side-by-side comparison of the rules, differences, and common pitfalls of both techniques.
- Covariant Return Type — Override a method and return a more specific (subclass) type than the parent declared.
- super Keyword — Access the parent class’s fields, methods, and constructors from within a subclass.
- Instance Initializer Block — Run shared initialization code before every constructor, regardless of which one is called.
- final Keyword — Prevent a class from being subclassed, a method from being overridden, or a variable from being reassigned.
- Static & Dynamic Binding — Understand when the JVM resolves a method call at compile time versus at runtime.
- instanceof Operator — Safely test an object’s type at runtime, including the modern pattern-matching form from Java 16.
- vtable & Dynamic Dispatch — A deep dive into the internal data structure the JVM uses to implement runtime polymorphism efficiently.
Related Topics
- Inheritance — Polymorphism builds directly on inheritance; understanding IS-A relationships is essential.
- Abstract Class — Abstract classes enforce a polymorphic contract while sharing common implementation.
- Interfaces — The most flexible way to achieve runtime polymorphism across unrelated class hierarchies.
- OOP Concepts — See how polymorphism fits alongside encapsulation, inheritance, and abstraction.
- Abstract Class vs Interface — Know when to use each as a polymorphic base type.
- Design Patterns — Many classic patterns (Strategy, Template Method, Factory) are polymorphism applied at the architectural level.