Polymorphism in Java
Understand runtime method dispatch, upcasting, downcasting, and instanceof pattern matching for flexible Java code.
Polymorphism in Java
Polymorphism — “many forms” — is the ability of objects to be treated as instances of their parent type while executing the correct subclass method. It is the mechanism that makes interfaces usable and inheritance powerful.
Introduction
Polymorphism is what makes object-oriented programming scalable. Without polymorphism, every function that operates on different shapes would need explicit type checks and casts: if (shape instanceof Circle) cast and call circle.area(). With polymorphism, you write shape.area() and the correct implementation is chosen at runtime based on the actual object type. This is the difference between code that scales with new types and code that requires modification every time a new type is added.
Java supports two forms of polymorphism at the language level. Compile-time polymorphism — method overloading and generics — resolves at compile time based on the declared types. Runtime polymorphism — method overriding — resolves at runtime via virtual method dispatch, where the JVM looks up the actual object’s type in its class’s method table (vtable) and calls the appropriate implementation. The runtime form is what most people mean when they say “polymorphism” in Java.
The practical impact of polymorphism reaches every Java codebase. Frameworks rely on it for dependency injection, callbacks, and extension points. Collections hold Object or parameterized types and call overridden methods on elements. REST controllers call service methods that return interface types, with the actual implementation chosen at runtime. If you write Java long enough, you are writing polymorphic code whether you realize it or not.
This guide covers the mechanics of upcasting and downcasting, how virtual method dispatch works via the vtable, pattern matching for instanceof (Java 16+), and the failure scenarios that cause ClassCastException, slicing, and infinite recursion in polymorphic code.
When to Use
Use polymorphism when:
- Writing code that operates on base types — functions that accept
Shapework forCircleandSquare - Adding new subtypes without modifying existing code — Open/Closed Principle
- Implementing services that delegate to different implementations — strategy pattern
- Building extensible frameworks — hooks and callbacks that subclasses customize
public class Shape {
public double area() {
return 0;
}
}
public class Circle extends Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public class Square extends Shape {
private final double side;
public Square(double side) {
this.side = side;
}
@Override
public double area() {
return side * side;
}
}
// Polymorphic usage
public double totalArea(List<Shape> shapes) {
double total = 0;
for (Shape shape : shapes) {
total += shape.area(); // Calls correct method for each type
}
return total;
}
List<Shape> shapes = List.of(new Circle(5), new Square(4));
System.out.println(totalArea(shapes)); // Works with both Circle and Square
When Not to Use
Avoid polymorphism when:
- Simple conditional logic — don’t create classes just to avoid if/else
- Unrelated types — don’t force common interface where none exists
- Performance-critical tight code — virtual dispatch has small overhead (usually negligible)
- Static behavior — methods that should never be overridden don’t need polymorphism
Polymorphism Types — Mermaid Diagram
flowchart TD
A[Compile-Time Polymorphism] -->|Method Overloading| B[same name, different params]
A -->|Generics| C[type parameters]
D[Runtime Polymorphism] -->|Method Overriding| E[dynamic dispatch via vtable]
D -->|Interface| F[different implementations same contract]
style A stroke:#333
style D stroke:#333
Failure Scenarios
1. Slicing — Losing Subclass Data
public class Base {
public int baseField = 1;
}
public class Derived extends Base {
public int derivedField = 2;
}
// Slicing — object is copied as Base, losing Derived parts
Base b = new Derived(); // Allowed: upcasting
System.out.println(b.baseField); // Works: 1
System.out.println(b.derivedField); // COMPILE ERROR: Base doesn't have derivedField
// Fix: if you need derived behavior, use the derived type
Derived d = new Derived(); // Or downcast carefully
2. Incorrect Downcasting
public class Animal { }
public class Dog extends Animal { }
public class Cat extends Animal { }
Animal animal = new Dog();
// Wrong downcast
Cat cat = (Cat) animal; // Compiles, but throws ClassCastException at runtime
3. Calling Non-Polymorphic Methods on References
public class Parent {
public void display() {
System.out.println("Parent.display()");
}
}
public class Child extends Parent {
public void display() {
System.out.println("Child.display()");
}
public void childOnlyMethod() {
System.out.println("Only Child has this");
}
}
Parent p = new Child();
p.display(); // Calls Child.display() — polymorphic
p.childOnlyMethod(); // COMPILE ERROR — Parent reference doesn't know this method
// Fix: downcast to access child-specific methods
((Child) p).childOnlyMethod(); // Works, but be sure of actual type
Trade-off Table
| Casting Type | Safe | Requires | Use When |
|---|---|---|---|
Upcasting (Derived → Base) | Always safe | Nothing | Storing different types in same collection |
Downcasting (Base → Derived) | Unsafe | instanceof check | Accessing subclass-specific methods |
Pattern matching (instanceof) | Safe | Pattern variable scoped | Java 16+ cleaner syntax |
record patterns | Safe | Pattern match | Java 16+ deconstruction |
Code Snippets
Safe Downcasting with instanceof (Pre-Java 16)
public void processShape(Shape shape) {
if (shape instanceof Circle) {
Circle circle = (Circle) shape; // Safe: checked above
System.out.println("Circle radius: " + circle.getRadius());
} else if (shape instanceof Square) {
Square square = (Square) shape;
System.out.println("Square side: " + square.getSide());
}
}
Pattern Matching for instanceof (Java 16+)
public void processShape(Shape shape) {
// Pattern variable 'circle' is scoped and typed within the block
if (shape instanceof Circle circle) {
System.out.println("Circle radius: " + circle.getRadius());
} else if (shape instanceof Square square) {
System.out.println("Square side: " + square.getSide());
}
}
// Can use in switch too (Java 21+)
public String describe(Shape shape) {
return switch (shape) {
case Circle c -> "Circle with radius " + c.getRadius();
case Square s -> "Square with side " + s.getSide();
case null, default -> "Unknown shape";
};
}
Virtual Method Dispatch
public class Greeter {
public void greet() {
System.out.println("Hello!");
}
}
public class SpanishGreeter extends Greeter {
@Override
public void greet() {
System.out.println("Hola!");
}
}
public class FrenchGreeter extends Greeter {
@Override
public void greet() {
System.out.println("Bonjour!");
}
}
// The correct method is chosen at runtime based on actual object type
Greeter g1 = new SpanishGreeter();
Greeter g2 = new FrenchGreeter();
g1.greet(); // Prints "Hola!" — SpanishGreeter.greet()
g2.greet(); // Prints "Bonjour!" — FrenchGreeter.greet()
Observability Checklist
- Methods intended for overriding marked
virtual(non-final) - Downcasting always preceded by
instanceofcheck (or pattern matching) - Avoid calling subclass-specific methods through base type references
- Prefer interfaces over concrete types for method parameters
- Test polymorphic behavior with different subtypes
Security Notes
- Don’t trust type at face value — use
instanceofbefore downcasting - Null checks —
instanceofreturns false for null, but verify null isn’t expected - Reflection bypass — can bypass normal polymorphism; avoid reflection for type-safe operations
- Malicious subclasses — overridden methods can behave unexpectedly; seal classes if needed
public class SecureProcessor {
// Use sealed classes to limit inheritance (Java 17+)
public sealed class Processor permits CreditProcessor, DebitProcessor {
public abstract void process(Transaction t);
}
public final class CreditProcessor extends Processor { }
public final class DebitProcessor extends Processor { }
public void handle(Processor p, Transaction t) {
// Compiler ensures only known subtypes can exist
// No unexpected subclasses possible
}
}
Pitfalls
- Slicing — when assigning subclass to superclass, fields specific to subclass are lost
- Type confusion — not checking actual type before downcasting causes ClassCastException
- Public fields in hierarchies — fields don’t participate in polymorphism, only methods do
- Confusing overloading with overriding — overloading is compile-time, overriding is runtime
- Forgetting that private methods are not overridden — they’re just hidden, not polymorphic
public class Base {
private void hidden() { System.out.println("Base.hidden"); }
public void call() { hidden(); } // Calls Base.hidden(), not overridable
}
public class Derived extends Base {
public void hidden() { System.out.println("Derived.hidden"); } // Not overriding — different method
}
Base b = new Derived();
b.call(); // Prints "Base.hidden" — private methods are not virtual in Java
Quick Recap
- Upcasting =
DerivedtoBase— always safe, implicit - Downcasting =
BasetoDerived— requiresinstanceofcheck to avoid ClassCastException - Virtual dispatch = method implementation chosen at runtime based on actual object type, not reference type
- Pattern matching = Java 16+ cleaner syntax for
instanceofwith automatic variable scoping - Only overridden methods participate in polymorphism — private and static methods are not polymorphic
Interview Questions
Model Answer: "Upcasting is converting a subclass reference to a parent class reference (e.g., Dog to Animal). This is always safe and done implicitly. Downcasting is converting a parent reference back to a subclass reference (e.g., Animal to Dog). This requires an instanceof check to avoid ClassCastException at runtime."
Model Answer: "Java uses a virtual method table (vtable) for each class. When an object is created, it carries a reference to its class's vtable. When a method is called on an object reference, Java looks up the actual object's type at runtime and calls the overridden method from its vtable, not the reference type's vtable."
Model Answer: "Overloading is compile-time polymorphism — multiple methods with the same name but different parameter lists in the same class. Overriding is runtime polymorphism — a subclass provides a specific implementation of a method already defined in its parent class, marked with @Override."
instanceof before downcasting?Model Answer: "Because a parent reference may point to an object of a different subclass type. Without checking, downcasting throws ClassCastException at runtime, which is a runtime error. Using instanceof (or pattern matching in modern Java) ensures you only cast when the actual object type supports it."
Model Answer: "No. Static methods belong to the class, not the instance, and are not polymorphic. If a subclass defines a static method with the same signature, it 'hides' the parent's method, it doesn't override it. Calling through a parent reference gets the parent's method; calling through a child reference gets the child's method."
Model Answer: "Virtual method table is a lookup table each class maintains for overridden methods. Every object carries a pointer to its class vtable at a fixed memory offset. When method is called, JVM looks up actual object type's vtable to find correct method address."
Model Answer: "Yes — through interfaces, which provide contract without requiring class hierarchy. Composition with interface-typed collaborators also achieves polymorphic behavior. Strategy pattern uses interface references that can point to different implementations."
Model Answer: "Early binding (static binding) happens at compile time — method resolution is static. Late binding (dynamic binding) happens at runtime — method resolved based on actual object type. Overloaded methods use early binding; overridden methods use late binding."
Model Answer: "Overload resolution happens at compile time based on argument types. Compiler determines which overloaded version to call based on declared argument types. Runtime polymorphism does not affect overloaded method selection."
Model Answer: "Final methods cannot be overridden — they use static binding (early binding). Subclass instances can still be stored in superclass variables (polymorphic storage). But the final method call resolves at compile time to the parent version."
Model Answer: "Open/Closed Principle: classes should be open for extension, closed for modification. Polymorphism enables extension by allowing new subclasses to be added without changing existing code. Code operates on abstractions (parent type) — new subtypes work without code changes."
@Override annotation and how does it relate to polymorphism?Model Answer: "@Override tells compiler to verify the method actually overrides a parent method. Catches typos in method names at compile time before runtime. Without @Override, a misspelled method would be a new method, not an override — breaking polymorphic behavior."
Model Answer: "Yes — any class implementing the interface can be assigned to interface reference regardless of when it was created. This is the essence of polymorphism — code depends on contract, not implementation date. Retroactive interface implementation is common when adding interfaces to existing classes."
Model Answer: "Polymorphism works at any hierarchy depth — object can be stored as any ancestor type. Deep hierarchies can complicate understanding but don't affect polymorphic mechanism. Virtual dispatch walks the chain at runtime to find the overridden method."
Model Answer: "Interface-typed parameters accept any implementation — more flexible. New implementations can be added later without changing the method signature. Example: List vs ArrayList — method accepting List can work with LinkedList, ArrayList, etc."
Model Answer: "Polymorphic assignment: storing subclass instance in superclass variable (upcasting). Always safe and implicit — no instanceof check needed. Opposite (downcasting) requires instanceof check to avoid ClassCastException."
Model Answer: "Java doesn't support multiple class inheritance, avoiding diamond problem for state. Interface default methods can cause diamond problem if two interfaces have same default method. Class must explicitly resolve which default to use via InterfaceName.super.methodName()."
Model Answer: "Pattern matching combines check and cast into one expression. Pattern variable is scoped within the if block — no accidental use outside. Cleaner than: if (obj instanceof String) { String s = (String) obj; ... }."
Model Answer: "Encapsulation protects internal state — prerequisite for safe inheritance. Inheritance enables code reuse and 'is-a' relationships. Polymorphism makes inheritance useful — code operates on abstractions, not concrete types. All three work together — polymorphism builds on encapsulation and inheritance."
Model Answer: "No — private methods are not visible to subclasses, so cannot be overridden. If subclass defines same-named private method, it's a new method, not an override. Private methods are resolved at compile time via static binding."
Further Reading
- Inheritance — subclassing and the extends keyword
- Abstract Classes — abstract base classes and methods
- Interfaces — defining contracts with interface types
- Encapsulation — data hiding with access modifiers
- Composition Over Inheritance — when to prefer composition
Summary
Polymorphism allows objects to be treated as instances of their parent type while executing the correct subclass method at runtime. Upcasting (Derived to Base) is always safe and implicit; downcasting (Base to Derived) requires an instanceof check to avoid ClassCastException. Virtual method dispatch uses the vtable mechanism to select the correct implementation based on the actual object’s type, not the reference type.
Compile-time polymorphism includes method overloading (same name, different parameters) and generics. Runtime polymorphism centers on method overriding, where subclasses provide specific implementations of parent methods. Pattern matching for instanceof (Java 16+) simplifies type checking with automatic variable scoping. Only overridden instance methods participate in polymorphism — private, static, and final methods do not.
Polymorphism is the mechanism that makes interfaces useful and inheritance powerful, allowing code to operate on abstractions rather than concrete types.
Category
Related Posts
Abstract Classes in Java
Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.
Arithmetic Operators in Java
Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.
Array Basics in Java
Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.