Inheritance & Polymorphism

Polymorphism & Dynamic Dispatch

15 min Lesson 5 of 14

Polymorphism & Dynamic Dispatch

You have learned how to create child classes and override methods. Now comes the idea that ties all of it together: polymorphism — the ability of one variable to behave like objects of many different types. Combined with dynamic dispatch, which is how Java decides at runtime which version of an overridden method to call, polymorphism becomes the backbone of flexible, extensible design.

A Parent Reference to a Child Object

In Java, a variable of a parent type can hold a reference to any child object. This is perfectly legal because a child is-a parent:

class Animal { public void speak() { System.out.println("..."); } } class Dog extends Animal { @Override public void speak() { System.out.println("Woof!"); } } class Cat extends Animal { @Override public void speak() { System.out.println("Meow!"); } } public class Main { public static void main(String[] args) { Animal a = new Dog(); // parent reference, child object a.speak(); // prints: Woof! a = new Cat(); // same variable, different object a.speak(); // prints: Meow! } }

The variable a is declared as Animal, but it is pointing at a Dog object. When speak() is called, Java does not use the Animal version — it uses the actual object's version. That choice happens at runtime, not at compile time. This is dynamic dispatch.

Dynamic dispatch in one sentence: Java looks at the actual type of the object in memory to decide which overridden method to run, regardless of the declared type of the variable holding it.

Why Does This Matter? Programming to a Type

The real power shows up when you write code that works with the parent type and therefore works with any present or future child. This principle is called programming to a type (sometimes phrased as "program to an interface, not an implementation").

public class ZooKeeper { // This method accepts ANY Animal — Dog, Cat, Lion, or anything added later public void makeAnimalSpeak(Animal animal) { animal.speak(); // dynamic dispatch picks the right method } } public class Main { public static void main(String[] args) { ZooKeeper keeper = new ZooKeeper(); keeper.makeAnimalSpeak(new Dog()); // Woof! keeper.makeAnimalSpeak(new Cat()); // Meow! } }

ZooKeeper.makeAnimalSpeak was written once and never needs to change, even if you add a Lion class tomorrow. The new class simply overrides speak(), and dynamic dispatch handles the rest.

Polymorphism with Arrays and Lists

Storing mixed child objects under a common parent type in a collection is a classic pattern:

import java.util.List; import java.util.ArrayList; public class Main { public static void main(String[] args) { List<Animal> animals = new ArrayList<>(); animals.add(new Dog()); animals.add(new Cat()); animals.add(new Dog()); for (Animal a : animals) { a.speak(); // each object answers with its own version } } }

The loop does not need to know or care about the concrete type. The output is:

Woof! Meow! Woof!
Declare variables at the highest useful type. Prefer Animal a = new Dog() over Dog a = new Dog() when the rest of the code only needs Animal behaviour. This lets you swap the concrete type later without touching every line that uses a.

What Dynamic Dispatch Does NOT Cover

Dynamic dispatch applies to instance methods only. It does not apply to:

  • Fields — if a parent and child both declare a field with the same name, the reference type determines which field is read, not the object type.
  • Static methods — static methods belong to the class, not to objects, so they are resolved at compile time based on the reference type.
Avoid hiding static methods. Declaring a static method in a child class with the same signature as one in the parent is called method hiding, not overriding. Dynamic dispatch does not apply, and the result depends on the reference type — a common source of subtle bugs.

A Concrete Example: Shape Area

Here is a self-contained example you can compile and run, showing exactly how dynamic dispatch selects the correct area() at runtime:

class Shape { public double area() { return 0.0; } } class Circle extends Shape { private double radius; Circle(double radius) { this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } } class Rectangle extends Shape { private double width, height; Rectangle(double width, double height) { this.width = width; this.height = height; } @Override public double area() { return width * height; } } public class Main { public static void printArea(Shape s) { System.out.printf("Area = %.2f%n", s.area()); } public static void main(String[] args) { printArea(new Circle(5)); // Area = 78.54 printArea(new Rectangle(4, 6)); // Area = 24.00 } }

printArea is written against Shape. It will work correctly for every Shape subclass ever created, without any changes.

Summary

  • A parent-type variable can hold a reference to any child object.
  • Dynamic dispatch ensures the overridden method of the actual object is called at runtime.
  • Programming to a type means writing code against a parent type so it works with all children — past and future.
  • Dynamic dispatch applies to instance methods only, not to fields or static methods.