SECTION 1
Historical Context & Motivation
When programmers first started writing large software systems, they ran into a frustrating problem: every time they wanted to add a new type of object, they had to rewrite large portions of existing code. Early languages like Fortran and C used procedural approaches, where the programmer had to explicitly check the type of every object before calling the right function. Imagine writing a drawing program where you had to use a giant chain of if and else statements just to decide whether to draw a circle, a square, or a triangle. Adding a new shape meant hunting through all that code and adding yet another branch. This was tedious, error-prone, and hard to maintain.
The concept of dynamic dispatch was developed to solve exactly this problem. Instead of the programmer writing all those type checks, the language itself figures out which version of a method to call based on the actual object at runtime — meaning while the program is actually running, not when the code is first compiled. This idea is one of the pillars of object-oriented programming (OOP), and Java uses it every time you override a method in a subclass.
1967 • Simula introduces classes & virtual methods
1972 • Smalltalk embraces message passing
1983 • C++ adds virtual functions
virtual to enable dynamic dispatch, giving them explicit control over when it occurred.1995 • Java makes dynamic dispatch the default
The central question that dynamic dispatch answers is: when a variable's declared type is a parent class but the actual object is a subclass, which version of an overridden method runs? Understanding the answer to this question unlocks the full power of polymorphism in Java.
SECTION 2
Core Principles & Definitions
Before diving deeper, let's nail down the key ideas that make dynamic dispatch work. These principles build on concepts you may already know from inheritance and method overriding, but they clarify why Java behaves the way it does at runtime.
Declared Type vs. Actual Type
new. Example: Animal a = new Dog(); — declared type is Animal, actual type is Dog.Method Overriding
Runtime Decision
Polymorphism in Action
KEY TAKEAWAY
SECTION 3
Visual Explanation
The diagram below shows exactly what happens when Java encounters a method call on a variable whose declared type is a superclass. Follow the arrows to see how the JVM traces from the variable to the actual object and selects the correct method implementation.
a is declared as Animal but holds a Dog object. When a.speak() is called, the JVM checks the actual type at runtime and selects Dog's version of speak(), not Animal's.Notice the dashed line going to Animal.speak() — that path is not taken. Even though the compiler sees the declared type Animal and confirms that speak() exists in Animal, the actual method that runs is determined by the actual object's class at runtime. This is the heart of dynamic dispatch.
SECTION 4
How Dynamic Dispatch Works Under the Hood
Dynamic dispatch doesn't require complex math, but it helps to understand the mechanism the JVM uses. When Java compiles your code, it creates a special lookup structure for each class called a virtual method table (often abbreviated as vtable). Think of a vtable as a small directory — like a contacts list on your phone — where each entry maps a method name to the actual code that should run for that class.
The Two-Phase Process
Dynamic dispatch involves two phases. In Phase 1 (compile time), the Java compiler checks the declared type of the variable to make sure the method you're calling actually exists. If you wrote Animal a = new Dog(); a.fetch(); and fetch() is only defined in Dog but not in Animal, the compiler will give you an error — even though the actual object is a Dog.
In Phase 2 (runtime), the JVM looks at the actual object in memory, finds its class, and consults that class's vtable to locate the correct method implementation. If the subclass overrides the method, the subclass version runs. If it doesn't, the parent's version runs. This lookup is very fast — usually just an index into an array.
Compile Time vs. Runtime Summary
| Phase | What the JVM Checks | Type Used |
|---|---|---|
| Compile time | Does the method exist in the declared type? | Declared type (left side of =) |
| Runtime | Which version of the method should run? | Actual type (right side, the new keyword) |
The two phases of dynamic dispatch
Important Exception
static methods, private methods, or final methods. These are resolved at compile time using static dispatch because they cannot be overridden.SECTION 5
How Dispatch Travels the Inheritance Chain
In real programs, you often have more than two levels of inheritance. A class might extend another class, which extends yet another. When dynamic dispatch kicks in, the JVM starts at the actual object's class and looks for the method there first. If the method isn't overridden in that class, the JVM moves up to the parent, then the grandparent, and so on — until it finds an implementation. Let's visualize this with a three-level hierarchy.
a.speak() is called, the JVM finds speak() in Puppy and stops. When a.eat() is called, the JVM doesn't find it in Puppy or Dog, so it walks up to Animal and uses that version.This walk-up-the-chain behavior is why dynamic dispatch is so powerful. You can add new subclasses at any level, override only the methods you need, and existing code that uses the parent type will automatically call the new subclass's methods — no changes required in the calling code. This is the open-closed principle in action: your code is open for extension but closed for modification.
SECTION 6
Worked Example: Shape Drawing Program
Let's trace through a complete example to see dynamic dispatch in action. We have a Shape superclass with subclasses Circle and Rectangle, each overriding the draw() method.
Predicting Output with Dynamic Dispatch
- 1
Step 1 — Read the class definitions
We define three classes.Shapehas a methoddraw()that prints "Drawing a shape".Circle extends Shapeand overridesdraw()to print "Drawing a circle".Rectangle extends Shapeand overridesdraw()to print "Drawing a rectangle". - 2
Step 2 — Identify the variable declarations
In our main method, we write:Shape s1 = new Circle();andShape s2 = new Rectangle();andShape s3 = new Shape();. All three variables have the declared typeShape, but their actual types differ.s1 → actual type Circle | s2 → actual type Rectangle | s3 → actual type Shape - 3
Step 3 — Trace s1.draw()
The compiler checks: does Shape have adraw()method? Yes, so the code compiles. At runtime, the JVM checks: what is s1's actual object? It's a Circle. Does Circle overridedraw()? Yes.Output: "Drawing a circle" - 4
Step 4 — Trace s2.draw()
Same compile-time check passes. At runtime, s2's actual type is Rectangle. Rectangle overridesdraw(), so the JVM uses Rectangle's version.Output: "Drawing a rectangle" - 5
Step 5 — Trace s3.draw()
Here, the actual type is also Shape (not a subclass). The JVM looks in Shape's vtable and finds Shape's owndraw()method. No overriding has occurred for this object.Output: "Drawing a shape" - 6
Step 6 — Put it all together
Even though all three variables are declared asShape, each one calls a different version ofdraw()based on the actual object. This is polymorphism powered by dynamic dispatch.Complete output: "Drawing a circle" → "Drawing a rectangle" → "Drawing a shape"
SECTION 7
Static Dispatch vs. Dynamic Dispatch
Not every method call in Java uses dynamic dispatch. It's important to understand when Java uses static dispatch (decided at compile time) versus dynamic dispatch (decided at runtime). Confusing the two is one of the most common mistakes on AP Computer Science exams and in Java programming.
| Feature | Static Dispatch | Dynamic Dispatch |
|---|---|---|
| When decided | Compile time | Runtime |
| Type used | Declared (compile-time) type | Actual (runtime) type |
| Applies to | static methods, private methods, final methods, overloaded methods | Overridden instance methods |
| Java keyword needed? | No — it's the default for static/private/final | No — it's the default for regular instance methods |
| Common confusion | Method overloading looks at declared types to pick the signature | Method overriding looks at actual type to pick the implementation |
Key differences between static and dynamic dispatch in Java
KEY TAKEAWAY
SECTION 8
Connection to Advanced Concepts
Dynamic dispatch is a foundational concept that shows up again and again in more advanced programming. Once you understand it, you'll be ready to tackle concepts like interfaces, abstract classes, and design patterns that rely heavily on polymorphic behavior.
| Concept You Know Now | Advanced Concept It Leads To |
|---|---|
| Dynamic dispatch with class inheritance | Interface dispatch — same mechanism works when a class implements an interface and you call methods through the interface type |
| Overriding a method in a subclass | Abstract methods — the parent class declares the method but provides no implementation, forcing every subclass to override it |
| Writing code that works on many subclass types | Strategy & Factory patterns — design patterns that exploit dynamic dispatch to swap behaviors at runtime |
| JVM vtable lookup | Reflection & dynamic proxies — advanced Java features that let programs inspect and modify dispatch behavior at runtime |
How dynamic dispatch connects to advanced Java topics
In college-level courses and real-world software engineering, you'll encounter the Liskov Substitution Principle, which states that any subclass object should be usable wherever a parent class object is expected. Dynamic dispatch is what makes this principle actually work in Java. Without it, polymorphism would be nothing more than a nice idea on paper.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
Animal a = new Cat(); a.makeSound(); where Animal has makeSound() printing "Some sound" and Cat overrides it to print "Meow!".Show answer
a is declared as type Animal, the actual object created with new Cat() is a Cat. Dynamic dispatch means the JVM checks the actual runtime type of the object (Cat), not the declared type of the variable (Animal), when deciding which version of makeSound() to execute. Since Cat overrides makeSound(), the Cat version runs, printing "Meow!".Question 2: BASIC
class Vehicle { void start() { System.out.println("Vehicle starting"); } }
class Car extends Vehicle { void start() { System.out.println("Car starting"); } }
Vehicle v = new Car(); v.start();Show answer
v has declared type Vehicle but actual type Car. Since Car overrides the start() method, dynamic dispatch ensures that the JVM calls Car's version of start() at runtime. The declared type Vehicle is only used at compile time to verify that start() exists.Question 3: INTERMEDIATE
A has method greet() printing "Hello from A". B extends A and overrides greet() to print "Hello from B". C extends B but does NOT override greet(). What does A x = new C(); x.greet(); print, and why?Show answer
greet(). C does not override it, so the JVM moves up to C's parent, B. B does override greet(), so B's version is used. The JVM never reaches A's version because the search stops as soon as it finds an overriding implementation. This demonstrates how dynamic dispatch walks up the inheritance chain from the actual type.Question 4: APPLIED
Notifier and subclasses EmailNotifier, SMSNotifier, and PushNotifier. Each overrides a send(String message) method. You store all notifiers in an array: Notifier[] notifiers = {new EmailNotifier(), new SMSNotifier(), new PushNotifier()}; and loop through calling n.send("Hello") on each. Explain how dynamic dispatch makes this design flexible and easy to extend.Show answer
send() method for each notifier without any type-checking code. The array is typed as Notifier[], but each element's actual type determines which send() runs. To add a new notification channel, say SlackNotifier, you only need to create the new subclass and add it to the array. The loop code never changes. This demonstrates the open-closed principle: the system is open for extension but closed for modification.Question 5: CRITICAL THINKING
class Parent { static void hello() { System.out.println("Parent"); } } and class Child extends Parent { static void hello() { System.out.println("Child"); } }. Now: Parent p = new Child(); p.hello(); — what prints and why? How does this differ from dynamic dispatch with instance methods?Show answer
hello() is static, the compiler looks at the declared type of p (which is Parent) and binds the call to Parent's hello() at compile time. The actual runtime type (Child) is irrelevant. This is called method hiding, not overriding. If hello() were an instance method instead, the output would be "Child" because dynamic dispatch would kick in and use the actual type. This is a critical distinction between static and instance methods in Java.SUMMARY
Lesson Summary
Summary
Dynamic dispatch is the mechanism by which Java determines which version of an overridden method to call at runtime. When a variable's declared type is a superclass but the actual object is a subclass, the JVM uses the actual object's class to find the correct method implementation. It searches from the actual class upward through the inheritance chain until it finds the method.
This mechanism powers polymorphism — the ability to write code that works with many different subclass types through a single parent type. Remember that static, private, and final methods use static dispatch instead, being resolved at compile time. The key rule is: overloading is static, overriding is dynamic. Mastering this distinction is essential for understanding Java's object-oriented behavior and for success on AP Computer Science exams.