Dynamic Dispatch

June 17, 2026 in fundamentals 4 minutes

A intermediate-level guide to Dynamic Dispatch: before-and-after java code and diagrams for a CS student.

The Maintenance Trap of Manual Type Checking

Imagine you are building a system to manage different types of animals. At first, it is simple: you have a Dog and a Cat. But as your application grows, you add a Bird, then a Lion, then a Fish. Every time you add a new animal, you find yourself hunting through your codebase to find every if-else block or switch statement that checks the type of an object before deciding what it should do.

This is a classic “Switch Statements” code smell. You are manually managing behavior based on types, which violates the Open/Closed Principle: your existing logic must be modified every time you introduce a new class.

class Dog {
    void makeSound() { System.out.println("Woof!"); }
}

class Cat {
    void makeSound() { System.out.println("Meow!"); }
}

public class AnimalService {
    // Manual type checking required to decide behavior
    void performSound(Object animal) {
        if (animal instanceof Dog) {
            ((Dog) animal).makeSound();
        } else if (animal instanceof Cat) {
            ((Cat) animal).makeSound();
        }
    }
}

In this version, AnimalService has no idea what an animal is; it only knows how to inspect them one by one using instanceof. The relationships are disconnected, making the code fragile and difficult to extend:

  classDiagram
    direction BT
    Dog ..> AnimalService : parameter
    Cat ..> AnimalService : parameter
    AnimalService --> Dog : casts to
    AnimalService --> Cat : casts to

Introducing Dynamic Dispatch

To fix this, we move the decision-making process from the service to the objects themselves. Instead of asking “Are you a Dog?”, the service simply says “Make your sound.” This shift is powered by dynamic dispatch (also known as polymorphism).

In dynamic dispatch, the specific method that gets executed is determined at runtime based on the actual type of the object, rather than the declared type used in the code.

interface Animal {
    void makeSound();
}

class Dog implements Animal {
    @Override
    public void makeSound() { System.out.println("Woof!"); }
}

class Cat implements Animal {
    @Override
    public void makeSound() { System.out.println("Meow!"); }
}

public class AnimalService {
    // Dynamic dispatch: the JVM determines which makeSound() to call at runtime
    void performSound(Animal animal) {
        animal.makeSound();
    }
}

By using an interface, we have decoupled the AnimalService from the specific implementations. The service now works with any class that implements Animal.

  classDiagram
    class Animal {
        +makeSound() void
    }
    Dog --|> Animal : implements
    Cat --|> Animal : implements
    AnimalService --> Animal : calls "makeSound()" on

Declared Type vs. Actual Type

A common point of confusion for students is the difference between what the compiler sees and what actually happens in memory.

  1. Declared Type (The Label): When you write Animal animal = new Dog();, the declared type is Animal. The compiler uses this to decide which methods you are allowed to call. Since the interface defines makeSound(), you can call it.
  2. Actual Type (The Identity): At runtime, the JVM looks at the object in memory and sees it is actually a Dog. This is the “actual type.”

When you call animal.makeSound(), dynamic dispatch performs a lookup to ensure the version of the method belonging to the actual type is the one that runs.

How the Dispatch Happens at Runtime

The following sequence shows how the AnimalService triggers a behavior without knowing the specific identity of the object it holds.

  sequenceDiagram
    participant AS as AnimalService
    participant D as Dog
    
    AS ->> D: makeSound()
    D -->> AS: (output) "Woof!"

Overloading vs. Overriding

It is important to distinguish between two terms that sound similar but behave differently:

  • Overloading (Static/Compile-time): Creating multiple methods in the same class with the same name but different parameters. The compiler decides which one to use when you write the code.
  • Overriding (Dynamic/Runtime): Providing a specific implementation of a method that is already defined in an interface or superclass. This is what enables dynamic dispatch; the decision of which version to run happens while the program is running.

When to Use It, and When Not To

Replace Conditional with Polymorphism whenever you have complex branching logic based on object types. This makes your code extensible: you can add a Cow class by simply implementing Animal, without ever touching the AnimalService code again.

However, do not use dynamic dispatch for everything. If you are dealing with simple data structures or primitive values (like integers or strings) where there is no meaningful behavior to vary, trying to force an inheritance hierarchy will only add unnecessary complexity and memory overhead.

Takeaways

  • Use dynamic dispatch to eliminate instanceof checks and manual type casting.
  • The declared type restricts what you can call; the actual type determines how that call behaves.
  • Dynamic dispatch is the mechanism that allows a system to be extended with new types without modifying existing business logic.

Usage

Demonstrating how the same method call results in different behaviors based on actual object type

AnimalService service = new AnimalService();
service.performSound(new Dog()); // Prints: Woof!
service.performSound(new Cat()); // Prints: Meow!