Read this method signature: void ignite(Engine e). It takes an Engine — not a petrol engine, not an electric one, just the contract. Yet when it runs, the right engine starts. Somehow the call e.start() reaches the correct start method for whatever object was handed in, even though ignite was written long before most of those engine types existed.
That behaviour has a name, and it is the single most important mechanism in object-oriented Java: dynamic dispatch. Without it, every new subclass would force you to open old methods and stitch in another branch. With it, you code against a supertype once, and every subtype you ever invent just works. This lesson teaches you to predict exactly which method runs, so a surprising dispatch never catches you out in production.
flowchart LR V["declared type<br/>Engine e"] --> O["runtime type<br/>ElectricEngine object"] style V fill:#3776ab,color:#fff style O fill:#c2410c,color:#fff
Two types, one variable
Every object variable in Java carries two types, and keeping them straight is the whole game. The declared type (sometimes called the static type) is the name you wrote to the left of the variable: the Engine in Engine e. The compiler checks against it — it decides which method calls are legal. The runtime type (the actual type) is the class of the object the variable currently points at: an ElectricEngine created by new. The JVM consults that one when a call runs.
A variable of declared type Engine may legally hold an ElectricEngine, because an electric engine is an engine. The compiler only ever sees the declared type; only the running program knows the real one. That gap between the two is where dispatch lives.
public class Main {
interface Engine {
String start();
}
static class PetrolEngine implements Engine {
@Override
public String start() {
return "Petrol engine: vroom";
}
}
static class ElectricEngine implements Engine {
@Override
public String start() {
return "Electric engine: hum";
}
}
public static void main(String[] args) {
Engine[] engines = { new PetrolEngine(), new ElectricEngine() };
for (Engine e : engines) {
System.out.println(e.start());
}
}
}
How dispatch picks the method
When you write e.start(), two things happen, in order. First, the compiler confirms that the declared type Engine has a start method — if it did not, the call would not compile, no matter what object e held. Second, at runtime, the JVM ignores the declared type entirely and asks the object itself: which start should run?
Every class keeps a hidden table of its method implementations. The JVM follows the object's real class to that table and invokes the entry it finds there. That lookup is dynamic dispatch. The declared type gates whether the call is legal; the runtime type decides which body executes.
flowchart TD
C["call e.start()"] --> Q{"look up start() on<br/>the runtime type"}
Q --> P["PetrolEngine.start"]
Q --> E["ElectricEngine.start"]
style Q fill:#c2410c,color:#fff
public class Main {
interface Engine {
String start();
}
static class PetrolEngine implements Engine {
@Override
public String start() {
return "vroom";
}
}
static class ElectricEngine implements Engine {
@Override
public String start() {
return "hum";
}
}
static void ignite(Engine e) {
System.out.println("Igniting: " + e.start());
}
public static void main(String[] args) {
Engine e = new ElectricEngine();
ignite(e);
ignite(new PetrolEngine());
}
}
Dispatch reaches up through super
An override can call back into the version it replaced using super.method(), and dispatch still does the right thing. A subclass method might return super.describe() + " extra", layering its own behaviour on top of the parent's without rewriting it.
The super. prefix means the parent's version specifically — it is one of the few places Java deliberately skips dynamic dispatch, because you usually want the parent body rather than your own (calling your own would recurse forever). Everywhere else, a plain method() call dispatches to the runtime type. Ordinary calls follow the object; super is the explicit, parent-bound exception.
What is NOT dynamically dispatched
Here is the part that trips up nearly everyone. Dynamic dispatch applies to instance methods, and only to instance methods. Two close neighbours look identical but are resolved at compile time, by the declared type.
Fields are chosen by the declared type of the expression, not by the object's real class. Static methods are too, because they belong to the class as a whole, not to any one instance. So reading a field or calling a static method never consults the runtime type — the compiler has already decided which one you mean when it built the program. Only a genuine instance method call goes through the runtime lookup. Hold onto that split and you will read Java correctly under pressure.
flowchart LR ACC["field access or static call"] --> S["static binding<br/>uses declared type"] CALL["instance method call"] --> D["dynamic dispatch<br/>uses runtime type"] style S fill:#b45309,color:#fff style D fill:#3776ab,color:#fff
public class Main {
static class Animal {
String tag = "animal";
String describe() {
return "animal method";
}
}
static class Dog extends Animal {
String tag = "dog";
@Override
String describe() {
return "dog method";
}
}
public static void main(String[] args) {
Animal a = new Dog();
System.out.println(a.tag);
System.out.println(a.describe());
}
}
What does this program print? Three engines in a list. Predict all three lines before you check.
public class Main {
interface Engine {
String start();
}
static class PetrolEngine implements Engine {
@Override
public String start() { return "vroom"; }
}
static class DieselEngine implements Engine {
@Override
public String start() { return "clatter"; }
}
static class ElectricEngine implements Engine {
@Override
public String start() { return "hum"; }
}
public static void main(String[] args) {
Engine[] engines = { new ElectricEngine(), new DieselEngine(), new PetrolEngine() };
for (Engine e : engines) {
System.out.println(e.start());
}
}
}
Each array element is a different class, but all implement
Engine.Dispatch follows the object's real type, so each
start()runs its own body.They are printed in array order: electric, then diesel, then petrol.
A variable is declared Engine e but assigned new ElectricEngine(). When you call e.start(), which method runs?
The declared type only decides whether the call is legal; the body that runs is chosen by the object's real runtime type. Since e points at an ElectricEngine, its start() is the one the JVM finds in that class's method table.
The field-hiding trap. p is declared Player but holds a Captain. Predict both lines — they surprise most people.
public class Main {
static class Player {
String team = "red";
String role() {
return "member";
}
}
static class Captain extends Player {
String team = "blue";
@Override
String role() {
return "leader";
}
}
public static void main(String[] args) {
Player p = new Captain();
System.out.println(p.team);
System.out.println(p.role());
}
}
Fields bind by the declared type, not the runtime type.
pis declaredPlayer.So
p.teamreads thePlayerfield, which is"red".Methods dispatch dynamically, so
p.role()runsCaptain's override.
A dispatch chain. Each subclass calls super and prepends a letter. Trace carefully and predict all three lines.
public class Main {
static class Base {
String id() {
return "B";
}
}
static class Mid extends Base {
@Override
String id() {
return "M" + super.id();
}
}
static class Top extends Mid {
@Override
String id() {
return "T" + super.id();
}
}
public static void main(String[] args) {
Base[] items = { new Base(), new Mid(), new Top() };
for (Base b : items) {
System.out.println(b.id());
}
}
}
Base.id()returns"B".Mid.id()is"M"plussuper.id(), which isBase.id()="B", giving"MB".Top.id()is"T"plussuper.id()=Mid.id()="MB", giving"TMB".
Animal a = new Dog();. Both Animal and Dog declare a static method kind(). Which kind() does a.kind() call?
Static methods are not dynamically dispatched. The compiler resolves the call using the declared type of the reference, which is Animal, so Animal.kind() runs regardless of the object being a Dog. Only instance methods use the runtime lookup.
Recap
- Every object variable has a declared type (what the compiler sees) and a runtime type (the object's real class).
- Dynamic dispatch chooses an instance method by the runtime type — so a supertype variable can call the correct subclass body.
super.method()is the exception: it deliberately calls the parent's version, skipping dispatch.- Fields and static methods do not dispatch dynamically; they bind by the declared type.
- Prefer methods over exposed fields so a subclass can override and dispatch gives you its answer.
Next you will meet abstract classes and interface default methods, and learn the exact rule Java uses to resolve a method when more than one candidate exists.