Definition
Two binding mechanisms determine which method code runs when a call is made.
- Static binding (early binding): resolved at compile time. The compiler looks at the reference type and binds directly. Used for
privatemethods (not inherited, no override),staticmethods (class-level, no vtable),finalmethods (sealed, no override), and overloaded methods (compiler picks signature by argument types). - Dynamic binding (late binding): resolved at runtime. The JVM looks at the actual object type in the heap and dispatches via the virtual method table (vtable). Used for overridden instance methods.
Parent p = new Child(); p.method()calls Child's version because the JVM checksp's actual type (Child) at runtime, not the variable type (Parent).- Static binding is faster (no vtable lookup). Dynamic binding enables polymorphism.
Static binding: the bank's directory at National Bank of India says for accounts, go to Counter 1. You look at the directory (compile time) and go there. It does not matter who is sitting at Counter 1 today. Dynamic binding: you call customer service and say I need help with my account. The system checks your actual account type (savings or current) at that moment and routes you to the right specialist. The routing happens at the moment of the call (runtime), not when the phone system was installed (compile time).
Understanding the Concepts
What is static binding vs dynamic binding?
Binding determines which method implementation is called. Static binding makes this decision at compile time. Dynamic binding makes it at runtime.
Which methods use static binding?
Four types of method calls use static binding: private, static, final, and overloaded.
package com.bank.core;
import java.math.BigDecimal;
public class Account {
// Static binding: private (not inherited, no vtable entry)
private void audit() { System.out.println("Account audit"); }
// Static binding: static (class-level, no vtable)
public static String getPolicy() { return "Standard Policy"; }
// Static binding: final (sealed, no override possible)
public final String getAccountId() { return "ACC-BASE"; }
// Dynamic binding: instance method (can be overridden, vtable dispatch)
public String getDescription() { return "Generic Account"; }
// Static binding: overloaded methods (compiler picks by arg types)
public void deposit(BigDecimal amount) { }
public void deposit(BigDecimal amount, String note) { }
public void callAudit() { audit(); } // private -> static binding
}
public class SavingsAccount extends Account {
@Override public String getDescription() { return "Savings Account"; }
public static String getPolicy() { return "Savings Policy"; } // hiding
}
Which methods use dynamic binding?
Overridden instance methods use dynamic binding. The JVM resolves them at runtime through the vtable.
Account ref = new SavingsAccount();
// Dynamic binding (runtime: actual type = SavingsAccount)
ref.getDescription();
// Compiler: Account has getDescription()? Yes -> valid.
// Runtime: JVM checks actual type -> SavingsAccount -> vtable -> SavingsAccount.getDescription()
// Result: "Savings Account" (Child's version)
// Static binding (compile-time: reference type = Account)
ref.getPolicy(); // "Standard Policy" (Account's static method)
ref.getAccountId(); // "ACC-BASE" (Account's final method)
ref.callAudit(); // "Account audit" (private method, static binding)
ref.deposit(amount); // compiler picks overload 1 at compile time
How does the vtable work internally?
The JVM creates a vtable for each class at class loading time. The vtable maps method signatures to actual method implementations.
// Account vtable:
// getDescription() -> Account.getDescription
// deposit(BigDecimal) -> Account.deposit
// SavingsAccount vtable:
// getDescription() -> SavingsAccount.getDescription (overridden)
// deposit(BigDecimal) -> Account.deposit (inherited, same)
// When: Account ref = new SavingsAccount()
// ref.getDescription() -> JVM looks at actual object -> SavingsAccount
// -> checks SavingsAccount vtable -> getDescription() -> SavingsAccount.getDescription()
// private, static, final methods are NOT in the vtable:
// -> resolved at compile time -> no vtable lookup -> faster
Why does Parent p = new Child(); p.method() call Child's version?
Because the compiler generates invokevirtual Parent.method() (valid: Parent has the method). At runtime, the JVM checks p's actual object type (Child), looks up Child's vtable, finds Child.method(), and calls it. The variable type (Parent) determined compile-time validity. The actual type (Child) determined runtime dispatch.
How do the bytecode instructions differ?
| Instruction | Binding | Used for |
|------------|---------|----------|
| invokevirtual | Dynamic (vtable) | Instance method calls |
| invokeinterface | Dynamic (itable) | Interface method calls |
| invokestatic | Static | Static method calls |
| invokespecial | Static | Constructors, super.method(), private methods |
What are common mistakes?
The most common mistake is assuming all method calls go through dynamic binding.
// WRONG assumption: private methods dispatch dynamically
// Private methods use invokespecial: static binding. No vtable.
// WRONG assumption: static methods dispatch dynamically
// Account ref = new SavingsAccount();
// ref.getPolicy() returns "Standard Policy" (Account's version)
// Static methods use invokestatic: no vtable lookup.
// WRONG assumption: final methods have overhead from vtable lookup
// The JVM can optimize final methods because no override exists.
What should a senior engineer know?
- All instance method calls go through dynamic binding (invokevirtual) except private, which uses invokespecial.
- HotSpot can devirtualize monomorphic call sites (only one implementation exists) and inline the method body for performance.
- The JIT compiler uses class hierarchy analysis (CHA) to determine if a virtual call can be devirtualized.
- Interface method calls use
invokeinterfacewith an itable (interface method table) instead of vtable, which is slightly slower.
Visual Overview
Account ref = new SavingsAccount(); STATIC BINDING DYNAMIC BINDING (compile-time) (runtime) ┌────────────────────┐ ┌────────────────────┐ │ ref.getPolicy() │ │ ref.getDescription()│ │ ref.getAccountId() │ │ │ │ ref.callAudit() │ │ JVM at runtime: │ │ │ │ actual type? │ │ Compiler binds to │ │ SavingsAccount │ │ Account at compile │ │ │ │ │ time. │ │ ▼ │ │ │ │ vtable lookup │ │ No vtable lookup. │ │ │ │ │ Faster. │ │ ▼ │ │ │ │ SavingsAccount │ │ │ │ .getDescription() │ └────────────────────┘ └────────────────────┘ METHOD TYPE -> BINDING private -> static (invokespecial) static -> static (invokestatic) final -> static (invokevirtual, but JVM can inline) overloaded -> static (compiler picks signature) overridden -> dynamic (invokevirtual, vtable dispatch)
Key Takeaway
Static binding resolves method calls at compile time. The compiler looks at the reference type and binds directly. Used for private methods (not inherited, no vtable entry), static methods (class-level, no vtable), final methods (sealed, JVM can inline), and overloaded methods (compiler picks signature by argument types). Dynamic binding resolves at runtime. The JVM looks at the actual object type in the heap and dispatches via the vtable. Used for overridden instance methods, which is the mechanism behind runtime polymorphism.
Parent p = new Child(); p.method() calls Child's version because the compiler generates invokevirtual Parent.method() (valid: Parent has the method). At runtime, the JVM checks p's actual object (Child), looks up Child's vtable, finds Child.method(), and calls it. The variable type (Parent) determined compile-time validity. The actual type (Child) determined runtime dispatch. This is why polymorphism works, and why static methods do not exhibit polymorphism (they use invokestatic which binds to the class at compile time, ignoring the actual object).
Remember one sentence: static binding resolves at compile time by reference type (private, static, final, overloaded), dynamic binding resolves at runtime by actual object type (overridden instance methods via vtable), and this is why p.method() calls Child's version when p holds a Child object.
---