skip to content

Explain dynamic method dispatch (late binding) in Java: what is bound at compile time versus runtime, and how does it differ from overloading?

level: middleimportance: must knowfreq 78%

answer

  1. Compile time: overload + signature by STATIC type
  2. Runtime: implementation by ACTUAL type (dynamic dispatch / vtable)
  3. static, private, final methods + fields = statically bound (non-virtual)
  4. Fields and static methods are HIDDEN, not overridden
  5. Never call an overridable method from a constructor

basics

~20 s

When you call an overridden method, Java decides at runtime which version to run based on the object's actual type, not the variable's declared type. This is dynamic dispatch. Overloading is different: the compiler picks among same-named methods at compile time using the argument types.

solid answer

~50 s

Java resolves overridden instance-method calls with dynamic dispatch, also called late binding. The compiler only verifies that the declared (static) type of the reference has a matching method signature; the actual implementation that runs is chosen at runtime from the object's real class. So if an Animal reference holds a Dog, animal.speak() runs Dog.speak(). This is what makes runtime polymorphism work. Contrast two things bound at compile time: overload resolution (choosing between speak(String) and speak(int) by the argument's static type) and field access and static methods (resolved by the declared type, not the runtime type). The practical consequences: overriding gives runtime behavior, overloading and field hiding do not. A classic trap is calling an overridable method from a constructor, because the subclass override runs before the subclass's own fields are initialized. private, static, and final methods are non-virtual and statically bound.

code

java · 15 lines
java
class Animal {
    String kind = "animal";          // field: statically bound
    String describe() { return "an animal"; }   // virtual
    static String tag() { return "A"; }          // hidden, not overridden
}
class Dog extends Animal {
    String kind = "dog";
    @Override String describe() { return "a dog"; }
    static String tag() { return "D"; }
}

Animal a = new Dog();
System.out.println(a.describe()); // "a dog"  -> dynamic dispatch (runtime type)
System.out.println(a.kind);      // "animal" -> field, STATIC type Animal
System.out.println(a.tag());     // "A"      -> static method, STATIC type

go deeper

for a junior

Knows that an overridden method runs the subclass version based on the actual object, and that @Override marks it.

for a middle

Cleanly separates compile-time overload resolution (static type) from runtime dispatch (actual type), and knows fields/static/private/final are statically bound.

for a senior

Explains the vtable mechanism, the constructor-calls-overridable-method pitfall, and field-hiding traps; reasons about non-virtual cases.

for a principal

Connects dynamic dispatch to design (Open/Closed, the Template Method pattern, JIT devirtualization/inlining of monomorphic call sites) and the performance implications of megamorphic call sites.

## Two questions every method call must answer When Java compiles and runs a call like `x.m(arg)`, two separate decisions happen: 1. **Which *signature*?** — *which overload* of `m` (by name + parameter types). This is **overload resolution**. 2. **Which *implementation*?** — *whose version* of that chosen method actually runs. The key insight is that these happen at **different times** and use **different type information**. ## Static type vs runtime type Every reference has two relevant types: - its **static type** (a.k.a. *declared* / *compile-time* type) — what the variable is declared as: `Animal a = new Dog();` → static type `Animal`. - its **runtime type** (a.k.a. *dynamic* / *actual* type) — the real class of the object: here `Dog`. ## Compile-time: overload resolution + signature check At **compile time**, the compiler uses only **static types**. Given `a.speak("hi")`, it: - looks at the static type `Animal`, confirms it has a `speak` method whose parameter list matches the *static* type of the argument, and - if several overloads exist (`speak(String)`, `speak(Object)`), picks the **most specific** one by the argument's *compile-time* type. This selection — **overloading** — is **compile-time polymorphism** / **static binding**. It is frozen into the bytecode; the runtime object type is irrelevant to which *overload* was chosen. ## Runtime: dynamic dispatch picks the implementation At **runtime**, for an *overridden* **instance method**, the JVM looks at the object's **runtime type** and invokes that class's version. This is **dynamic method dispatch** / **late binding** / **runtime polymorphism**. Mechanically the JVM uses each object's reference to its class metadata and a per-class **method table (vtable)** to find the right implementation. So: ```java Animal a = new Dog(); a.speak(); // compiler checked Animal.speak() exists; JVM runs Dog.speak() ``` ## What is NOT dynamically dispatched Three categories are **statically bound** — resolved by the *declared* type, ignoring the runtime object: 1. **`static` methods** — they are *hidden*, not overridden. `Animal.create()` vs `Dog.create()` is chosen by the reference's static type. 2. **`private` and `final` methods** — they cannot be overridden, so the compiler binds them directly (they are *non-virtual*). 3. **Fields** — field access is resolved by the **static type**. `((Animal)dog).name` reads `Animal`'s `name` field even if `Dog` declares its own `name`. This is **field hiding**, and it is a frequent trap. ## Overriding vs overloading vs hiding — the comparison | Aspect | Overriding | Overloading | Hiding (static/field) | |---|---|---|---| | Same name? | yes | yes | yes | | Differs by | same signature, subclass | parameter list | static method / field in subclass | | Resolved | **runtime** (dynamic) | **compile time** (static) | **compile time** (static) | | Polymorphic? | yes | no | no | ## The constructor pitfall Because dispatch is dynamic, calling an **overridable method from a constructor** is dangerous. During `new Dog()`, the `Animal` constructor runs first; if it calls an overridable `init()`, the JVM dispatches to **`Dog.init()`** — but `Dog`'s own fields are **not yet initialized** (they're still default `0`/`null`). The override sees a half-built object. Rule: constructors should only call `private`, `static`, or `final` methods. ## Why this matters Dynamic dispatch is the engine of polymorphism: it lets you write code against a supertype (`List`, `Animal`, an interface) and have the correct concrete behavior chosen at runtime, enabling extension without modifying existing code (the Open/Closed Principle).

  • Given Animal a = new Dog(); does a.kind read Dog's or Animal's field, and why?
    It reads Animal's field. Field access is resolved by the static (declared) type of the reference, which is Animal — fields are hidden, not overridden, so dynamic dispatch does not apply.
  • Why are private methods not subject to dynamic dispatch?
    Private methods are not inherited or visible to subclasses, so they can never be overridden. The compiler can bind the call directly to the declared method — it is non-virtual / statically bound.

Ordering 'the special' at a restaurant: the menu (compile-time) guarantees 'the special' exists, but which dish actually arrives depends on the day (runtime). The waiter resolves the word 'special' to today's concrete dish only when you actually order.

saying these in an interview costs you the question

  • Believing field access is polymorphic — fields resolve by static type, not runtime type.
  • Thinking overloading is resolved at runtime — it is compile-time, by the argument's static type.
  • Calling overridable methods from constructors and assuming subclass fields are already set.
  • Claiming static methods are overridden — they are hidden, and the call is bound by the reference's static type.

context