Dynamic method dispatch is the rule that a method call through a supertype reference runs the implementation belonging to the actual object on the heap, not the one belonging to the variable type you wrote.
PaymentProcessor ref = new StripeProcessor();
ref.process("125.00");
// runs the StripeProcessor version
That call is settled in two separate stages, and keeping them apart is the key to the whole topic.
Compile time answers a question about the source code: does the method exist, and is it visible, on the type of the variable? Runtime answers a question about the object: which class actually sits behind that reference, and therefore which implementation belongs to it?
The compiler checks only the first. If process did not exist on PaymentProcessor, the program would not compile. If it does exist there, the compiler is satisfied and stops caring, because at that point it cannot know what will be assigned later.
When the program runs, the JVM reads the object's real class from the object header, follows that class's method table, and invokes the matching implementation. Nothing in the source code selected it. That is why the same line of code can print different messages depending on the object it receives, which is exactly what makes an open/closed design possible.
The most important caveat is that dispatch applies to instance methods only. Fields, static methods, and super calls are all bound at compile time, so the same object can present two different answers depending on how you reach it.
Detailed Explanation & Mechanics
1) The Two Stages of a Call
Every virtual call is resolved in two stages, and the table below is worth memorising because most misunderstandings come from conflating them.
Question
Answered by
Uses
Does the method exist and is it visible?
Compiler
The declared type of the variable
Which overload do the arguments select?
Compiler
The declared types of the arguments
Which implementation should run?
JVM
The actual class of the object
Note that the first two are decided entirely by types you can read in the source, and the third is the only one that depends on a value.
public abstract class PaymentProcessor {
public void process(String amount) {
System.out.println("processing generic " + amount);
}
}public class StripeProcessor extends PaymentProcessor {
@Override public void process(String amount) {
System.out.println("processing stripe " + amount);
}
}public class Checkout {
public static void pay(PaymentProcessor processor, String amount) {
processor.process(amount);
}
}
Given the call processor.process(amount), the compiler first reads the declared type of processor, which is PaymentProcessor. It finds process(String) there, confirms visibility, and emits a call. It does not inspect the object, because at that point the object may not exist yet.
At runtime the JVM performs the real selection:
It loads the value sitting in the local variable, which is a reference.
It follows the reference to the object on the heap.
It reads the class recorded in that object header, here StripeProcessor.
It looks up process(String) in that class's method table, following the inheritance chain upward only if the class itself does not define it.
It invokes that implementation.
The lookup walks upward, never downward. If PremiumProcessor extends StripeProcessor but does not override process, the inherited StripeProcessor implementation runs.
3) What the Variable Type Still Controls
Dispatch picks the implementation, but the compiler still uses the declared type for two things: deciding that the method is callable at all, and deciding which overload applies.
PaymentProcessor ref = new StripeProcessor();
ref.process("125.00");
// legal, the compiler found process on the type
ref.refund();
// COMPILE ERROR, StripeProcessor cannot widen the type
So dispatch does not remove the need for a correctly typed reference. It only removes the need for one that names the exact runtime class.
Overload selection is a good illustration, because it stays entirely at compile time even when overrides are in play.
PaymentProcessor ref = new StripeProcessor();
ref.process("125.00");
// stripe 125.00, the override wins at runtime
ref.process("125.00", true);
// urgent 125.00, the child has no such method
The second call picks the two-argument overload at compile time and, finding no override for it, ends up invoking the inherited parent version.
4) What Does Not Dispatch
This is the part beginners most often get wrong. Four categories are bound at compile time and ignore the runtime type entirely.
Category
Bound to
Reason
Instance fields
Declared type
A field is data, not behaviour
static methods
Declared type
Belong to the class, not the object
private methods
The class itself
Not overridable
super calls
The direct parent
Dispatch is bypassed on purpose
Fields are the sharpest edge. A child may declare a field with the same name and a different value, and the reference type decides which one you read.
public class PaymentProcessor {
protected String gateway = "generic";
}public class StripeProcessor extends PaymentProcessor {
protected String gateway = "stripe";
// a separate field, not an override
}StripeProcessor stripe = new StripeProcessor();
PaymentProcessor ref = stripe;
System.out.println(stripe.gateway);
// stripe
System.out.println(ref.gateway);
// generic
Both lines read a real field on the same object. The difference is purely the type of the expression used to reach it, which is why field hiding is confusing and why private fields with accessors are the usual advice.
Calling super.method() looks like it should still dispatch, but it cannot, because its entire purpose is to reach the parent implementation specifically.
public class StripeProcessor extends PaymentProcessor {
@Override public void process(String amount) {
super.process(amount);
// explicitly the parent version
System.out.println("logged the override");
}
}
The compiler knows exactly which parent method is meant, so it emits a direct, non-virtual call. This is why recursion is impossible here and why this pattern is the supported way to extend rather than replace parent behaviour.
6) Constructors Are Not Dispatched Either
This is the one most likely to cause a real bug. A constructor may call an overridable method, and the child version will run, before the child has finished initialising.
new StripeProcessor();
// prints: stripe gateway null
Why null rather than stripe? Because the child field initialiser runs after the parent constructor completes. The object already reports itself as a StripeProcessor, so dispatch reaches the child override, but the child fields are not yet assigned.
Never call an overridable method from a constructor. If a subclass overrides it and depends on its own fields, you will read uninitialised state. Make such methods final, or move the call into an explicit start() method the caller invokes.
7) How the JVM Implements It
Looking up a method by searching the class hierarchy on every call would be slow. Instead each class is prepared with a table of virtual method slots, often called a vtable, holding pointers to the concrete implementations.
Class
Slot for process
PaymentProcessor
PaymentProcessor.process
StripeProcessor
StripeProcessor.process
PayPalProcessor
PayPalProcessor.process
PremiumProcessor
Inherits the StripeProcessor pointer
Dispatch becomes a single indexed read: take the class pointer from the object header, index its table, and call what it finds. When a class overrides a method, the copying step simply leaves the child pointer in that slot, which is why no per-call searching is needed.
The JIT compiler then goes further. When a call site only ever sees one receiver class, a technique called monomorphic inlining, it can inline that implementation and remove the lookup entirely. When it regularly sees exactly two classes, bimorphic inlining, it keeps a fast two-way test. Beyond that it usually falls back to the full table lookup. This is why a hot loop that dispatches across dozens of classes performs worse than one over a handful.
8) Interfaces Dispatch Too
Dispatch is not limited to classes. An interface reference selects the implementation just as a class reference does, including for default methods.
public interface Priced {
String channel();
default String summary() {
return "channel=" + channel();
}
}public class StripeProcessor implements Priced {
@Override public String channel() {
return "stripe";
}
}Priced priced = new StripeProcessor();
System.out.println(priced.summary());
// channel=stripe
The interesting part is the nested call. summary() runs as a default method, then internally calls channel(), and that call dispatches to the implementation. A single invocation can therefore contain more than one runtime decision.
The interfaces topic covers the resolution order when a class inherits defaults from several interfaces.
9) Collections and Heterogeneous Lists
The real payoff is that a list declared with a supertype element can hold unrelated subtypes, and each element still dispatches correctly without the loop knowing anything about concrete types.
List<PaymentProcessor> processors = List.of( new PayPalProcessor(), new StripeProcessor());
for (PaymentProcessor processor : processors) {
processor.process("125.00");
}
The compiler checks process against PaymentProcessor once. Each iteration then makes its own runtime decision, which is precisely why you can add a new processor later without touching the loop.
10) Reflection Is Late Binding by Design
Ordinary dispatch resolves against the static type at compile time and the object type at runtime. Reflection discards the static type and resolves everything at runtime, which is what makes frameworks able to call methods a compiler never saw.
Here the lookup starts from PaymentProcessor.class, finds the slot, and then dispatches to StripeProcessor.process because that is the receiver. The reflection topic covers the performance cost, which is substantial compared with a virtual call.
11) Common Misconceptions
Claim
Verdict
Fields dispatch dynamically
False. Field access binds to the declared type.
Static methods dispatch dynamically
False. They bind to the declared type.
super calls dispatch
False. They deliberately bypass dispatch.
Overloading is decided at runtime
False. Argument types decide it at compile time.
Dispatch needs a cast
False. The JVM does it for you.
Dispatch fails on interfaces
False. Interface references dispatch normally.
A non-overriding child method runs instead
False. Dispatch climbs to the nearest ancestor that defines it.
Dispatch needs a vtable scan per call
False. A prepared table gives a direct slot lookup.
The compiler resolves names and overloads from types; the JVM resolves the implementation from the object.
Frequently Asked Questions
What exactly is dynamic method dispatch?
It is the rule that a call to an overridable instance method executes the implementation belonging to the runtime class of the receiver, rather than the one on the type of the variable. The variable type only has to be a supertype. It is the mechanism that makes an Animal reference able to run a Dog method.
When does the JVM decide which method to run?
At the moment the instruction executes, not when the class is compiled. The compiler emits the call but cannot know the object yet. At runtime the JVM reads the class from the object header and resolves the matching implementation, so the same line of code can produce different behaviour on different runs.
Are fields dispatched dynamically?
No. A field reference is bound to the declared type of the expression, so a supertype reference reads the parent field even when the object is a subclass with its own field of the same name. Only method behaviour dispatches. This asymmetry is the usual reason field hiding is discouraged in favour of accessors.
What happens if a subclass does not override the method?
The JVM climbs the inheritance chain and uses the nearest ancestor that defines it. So a class three levels deep with no override still runs the grandparent implementation, and callers never need to know which level supplied it. Dispatch always moves toward the most specific definition, never back down.
Does overloading work with dynamic dispatch?
Overload selection is finished at compile time, using the declared types of the arguments, and dispatch only applies afterwards. If the chosen overload is overridden in the subclass, the subclass version runs. If it is not overridden, the inherited parent version runs, which is why a child can add overloads without disturbing the ones it inherits.
Why should overridable methods never be called from a constructor?
Because the child override runs before the child initialisers have executed, so it can read fields that are still null or zero. The object already reports its final class, so dispatch succeeds, but the state it depends on does not exist yet. Make such methods final, or defer the call until after construction.
Is reflection the same as dynamic dispatch?
No. Ordinary dispatch keeps a compile-time link to a statically known method and resolves only the implementation at runtime. Reflection discards the compile-time link entirely and resolves the name, the parameters, and the implementation all at runtime. That extra flexibility is why frameworks depend on it, and it is also why it costs considerably more than a virtual call.
Does dispatch work through an interface reference?
Yes. An interface reference selects the implementing class exactly as a class reference does, and default methods dispatch as well. A default method that calls an abstract method produces two separate runtime decisions in one invocation, one for each method.
Is dynamic dispatch slow?
Not in practice. A virtual call is an indexed table lookup rather than a search, and the JIT compiler usually removes it entirely for a call site that only ever sees one receiver class. Performance problems appear when a single hot call site sees a very large number of distinct types, since the compiler can no longer specialise it.
Runnable Code Example: Dynamic Method Dispatch in Java
// Shows the two stages of a call, and the four things that
// do NOT dispatch even though dispatch is happening nearby.
import java.lang.reflect.Method;
import java.util.List;
public class DynamicDispatchDemo {
private static final String AMOUNT = "125.00";
private static final String GENERIC = "generic";
// AN INTERFACE REFERENCE DISPATCHES EXACTLY LIKE A CLASS REFERENCE
public interface Priced {
String channel();
default String summary() {
return "channel=" + channel();
// this inner call dispatches too
}
} public abstract static class PaymentProcessor implements Priced {
// A FIELD, SO IT IS BOUND BY THE DECLARED TYPE AND NEVER DISPATCHES
protected String gateway = GENERIC;
public PaymentProcessor() {
// DISPATCH REACHES THE CHILD OVERRIDE, BUT THE CHILD FIELD IS
// STILL NULL BECAUSE ITS INITIALISER RUNS AFTER THIS RETURNS
System.out.println("ctor sees " + summary());
} public void process(String amount) {
System.out.println("processing generic " + amount);
} @Override public String channel() {
return GENERIC;
} public static String kind() {
return "generic-processor";
} // ONE CALL, TWO DIFFERENT DECISIONS
public void report(String amount) {
System.out.println("field via supertype " + gateway);
System.out.println("summary " + summary());
process(amount);
}
} public static class PayPalProcessor extends PaymentProcessor {
protected String gateway = "paypal";
@Override public void process(String amount) {
System.out.println("processing paypal " + amount);
} @Override public String channel() {
return "paypal";
} public static String kind() {
return "paypal-processor";
}
} public static class StripeProcessor extends PaymentProcessor {
protected String gateway = "stripe";
public StripeProcessor() {
super();
} @Override public void process(String amount) {
super.process(amount);
// A DIRECT CALL, DISPATCH BYPASSED
System.out.println("processing stripe " + amount);
} @Override public String channel() {
return "stripe";
} public static String kind() {
return "stripe-processor";
}
} // NO OVERRIDE, SO DISPATCH CLIMBS TO THE STRIPE IMPLEMENTATION
public static class PremiumProcessor extends StripeProcessor {
} public static void pay(PaymentProcessor processor, String amount) {
processor.process(amount);
// the one line the compiler sees
} public static void main(String[] args) {
// BOTH CONSTRUCTORS DISPATCH, AND THE FIELD IS NOT READY YET
PaymentProcessor paypal = new PayPalProcessor();
PaymentProcessor stripe = new StripeProcessor();
System.out.println();
System.out.println("one method, three receivers:");
paypal.report(AMOUNT);
stripe.report(AMOUNT);
pay(new PremiumProcessor(), AMOUNT);
// THE SAME OBJECT, REACHED THROUGH TWO DIFFERENT TYPES
System.out.println();
System.out.println("fields and statics do not dispatch:");
StripeProcessor direct = new StripeProcessor();
PaymentProcessor indirect = direct;
System.out.println("as subclass " + direct.gateway);
System.out.println("as supertype " + indirect.gateway);
System.out.println("static on supertype " + PaymentProcessor.kind());
System.out.println("static on subclass " + StripeProcessor.kind());
// A LIST OF SUPERTYPES, EACH ELEMENT DISPATCHING ON ITS OWN
System.out.println();
System.out.println("heterogeneous list:");
List<PaymentProcessor> processors = List.of(new PayPalProcessor(), new StripeProcessor(), new PremiumProcessor());
for (PaymentProcessor processor : processors) {
processor.process(AMOUNT);
} // AN INTERFACE REFERENCE DISPATCHES THE SAME WAY
System.out.println();
System.out.println("through an interface:");
Priced priced = new StripeProcessor();
System.out.println(priced.summary());
// REFLECTION IS LATE BINDING, IT RESOLVES THE NAME AT RUNTIME
System.out.println();
System.out.println("through reflection:");
Method method = PaymentProcessor.class.getMethod("process", String.class);
Object returned = method.invoke(new StripeProcessor(), AMOUNT);
System.out.println("returned " + returned);
/*
ACTUAL OUTPUT:
ctor sees channel=paypal
ctor sees channel=stripe
ctor sees channel=paypal
ctor sees channel=null
one method, three receivers:
field via supertype generic
summary channel=paypal
processing paypal 125.00
field via supertype generic
summary channel=stripe
processing generic 125.00
processing stripe 125.00
processing stripe 125.00
fields and statics do not dispatch:
as subclass stripe
as supertype generic
static on supertype generic-processor
static on subclass stripe-processor
heterogeneous list:
processing paypal 125.00
processing stripe 125.00
processing stripe 125.00
through an interface:
channel=stripe
through reflection:
processing stripe 125.00
returned null
READING THE OUTPUT
1) THE CONSTRUCTOR LINES PROVE TWO THINGS AT ONCE. Dispatch
reached the child override, so the class was already
StripeProcessor, yet channel was still null because the
field initialiser had not run.
2) field via supertype reads generic in BOTH cases, because
the call goes through a PaymentProcessor reference.
Reading it as a subclass gives stripe. Same object,
different answer, decided entirely by the static type.
3) static on supertype and static on subclass differ for the
same reason, so static methods are never dispatched.
4) summary ran the child's channel, which is why the two
report lines disagree. Only process dispatched.
5) PremiumProcessor printed stripe, because it inherits the
implementation rather than replacing it.
*/
}
}
Common Java Technical Interview Questions & Answers
Interview Prep
Q1: What is a vtable in the JVM?\nA1: An internal table prepared per class, holding pointers to the concrete implementations of its virtual instance methods. Because it is built once when the class is linked, a virtual call becomes an indexed lookup rather than a search up the hierarchy. Overriding simply leaves a different pointer in the child slot, and a subclass that does not override inherits the parent pointer.\nQ2: Are fields resolved via dynamic dispatch?\nA2: No. A field reference is bound to the declared type of the expression, not the runtime type of the object. If a subclass declares a field with the same name, reading it through a supertype reference still returns the parent field. Static methods behave the same way, and super calls bypass dispatch deliberately.\nQ3: At what exact moment does the JVM choose the implementation?\nA3: When the call instruction executes, not when the class is compiled or even when it is loaded. The compiler emits the call after checking the method exists on the declared type. At runtime the JVM follows the reference, reads the class from the object header, and resolves the matching implementation, so the same bytecode can behave differently on different runs.\nQ4: Why can a constructor not safely call an overridable method?\nA4: Because the object already reports its final runtime class, so dispatch reaches the child override, but the child field initialisers have not executed yet. The override therefore reads null or zero values. Make such methods final or defer the call until after construction.\nQ5: Is overload resolution part of dynamic dispatch?\nA5: No. Overload selection is decided at compile time from the declared types of the arguments. Dispatch only applies to the implementation of the method already selected. If the chosen overload is overridden the child version runs, and if not the inherited parent version runs.\nQ6: Does dynamic dispatch work through an interface reference?\nA6: Yes. An interface reference selects the implementing class in exactly the same way a class reference does, and default methods dispatch as well. A default method that calls an abstract method produces two independent runtime decisions within a single invocation.\nQ7: How expensive is a dynamically dispatched call?\nA7: Much less than beginners assume. A virtual call is one table lookup, and the JIT compiler usually removes it entirely for a call site that only ever sees a single receiver class, a monomorphic site. A site that consistently sees two classes still gets a fast two-way test. Costs only become visible when one call site sees a very large number of distinct types.\nQ8: What happens when a subclass does not override the dispatched method?\nA8: The JVM resolves upward through the inheritance chain and invokes the nearest ancestor that defines it. So a class several levels deep with no override still runs the grandparent implementation, and callers never need to know which level provided it.\nQ9: How is dispatch different from reflection?\nA9: Ordinary dispatch keeps a compile-time link to a statically known method and resolves only the implementation at runtime. Reflection discards the compile-time link and resolves the name, the parameter types, and the implementation all at runtime, which is what allows frameworks to call methods a compiler never saw, at a significantly higher cost.\nQ10: Why is calling super a useful pattern rather than a way to bypass a bug?\nA10: Because super invokes the parent implementation directly without dispatch, which is exactly its purpose. It lets a subclass extend the parent behaviour instead of duplicating it, and because it is a non-virtual call it cannot recurse into the override that invoked it.
Key Takeaways, Best Practices & Clean Code Rules
Java Best Practice & Production Rules
Declare parameters and variables with the abstract base type or interface.\nRely on dispatch so new subclasses need no changes to client code.\nNever assume the runtime type from the variable type in a field access.\nKeep fields private and expose accessors to avoid field hiding.\nDo not expect static methods or constructors to dispatch.\nNever call an overridable method from a constructor.\nMark methods final when a subclass must not change behaviour.\nAvoid shadowed field names, since they read as overrides but are not.\nKeep interfaces small so implementations stay easy to provide.\nUse super to extend parent behaviour rather than copying its code.\nTest through a base type reference to confirm dispatch works as intended.\nDo not overload merely to change the implementation, that is overriding.\nKeep the number of types at a hot call site modest for the JIT.\nPrefer ordinary calls over reflection in performance-sensitive paths.\nMake a default method only when every implementation can share it.\nDocument a contract when a subclass must call super from an override.
Share with friends, students, or your developer network
We value your privacy & transparency
OperateTools uses cookies and local storage to ensure basic tool functionality, preserve preferences, and analyze aggregated traffic. We never sell your personal data or index your document files. Learn more in our Privacy Policy.
Stay Updated
Enable Notifications?
Get real-time updates, instant tool alerts & daily news directly on your device.