Static & Dynamic Binding
Binding is the process of connecting a method call (or field access) to the actual code that will run. Java performs this connection at two different moments — either at compile time (static binding) or at runtime (dynamic binding) — and knowing the difference is key to understanding how polymorphism really works under the hood.
What Is Binding?
When you write obj.doSomething(), Java needs to figure out which doSomething to execute. That decision can happen:
- Early (at compile time) — the compiler locks in the exact method. This is called static binding or early binding.
- Late (at runtime) — the JVM checks the actual object type and picks the method then. This is called dynamic binding or late binding.
A quick summary:
| Feature | Static Binding | Dynamic Binding |
|---|---|---|
| Resolved by | Compiler | JVM (at runtime) |
| Also called | Early binding | Late binding |
| Methods involved | private, static, final | Overridden instance methods |
| Performance | Slightly faster | Tiny overhead (vtable lookup) |
| Enables polymorphism? | No | Yes |
Static Binding
Static binding happens when the compiler can determine at compile time exactly which method or field to use. Three types of methods are always statically bound:
privatemethods — not visible outside the class; can never be overridden.staticmethods — belong to the class, not an instance; resolved by the declared type.finalmethods — cannot be overridden; the compiler knows there is only one version.
class Animal {
// static binding — private method
private void breathe() {
System.out.println("Animal breathes");
}
// static binding — static method
static void classify() {
System.out.println("I am an Animal (static)");
}
// static binding — final method
final void sleep() {
System.out.println("Animal sleeps");
}
void show() {
breathe(); // resolved at compile time
}
}
class Dog extends Animal {
// This is a NEW method — it does NOT override breathe()
// because breathe() is private in Animal
private void breathe() {
System.out.println("Dog breathes");
}
// static method "hiding", not overriding
static void classify() {
System.out.println("I am a Dog (static)");
}
}
public class StaticBindingDemo {
public static void main(String[] args) {
Animal a = new Dog();
a.show(); // calls Animal.breathe() — static binding
a.classify(); // calls Animal.classify() — based on reference type
a.sleep(); // calls Animal.sleep() — final, statically bound
Dog d = new Dog();
d.classify(); // calls Dog.classify()
}
}
Output:
Animal breathes
I am an Animal (static)
Animal sleeps
I am a Dog (static)
Notice that even though a holds a Dog object, a.classify() still calls Animal.classify(). Static methods are resolved on the reference type, not the actual object — this is sometimes called method hiding, not overriding.
Warning: Calling a
staticmethod through an object reference (likea.classify()) compiles fine but is misleading. Always call static methods on the class name (Animal.classify()) to make the binding intent clear.
Dynamic Binding
Dynamic binding is the default behavior for all non-private, non-static, non-final instance methods. The compiler does not lock in the method — instead it leaves a placeholder, and the JVM resolves it at runtime by inspecting the actual type of the object on the heap.
This is what makes runtime polymorphism possible. See Runtime Polymorphism for the full picture.
class Shape {
// dynamic binding candidate — can be overridden
void draw() {
System.out.println("Drawing a Shape");
}
}
class Circle extends Shape {
@Override
void draw() {
System.out.println("Drawing a Circle");
}
}
class Rectangle extends Shape {
@Override
void draw() {
System.out.println("Drawing a Rectangle");
}
}
public class DynamicBindingDemo {
public static void main(String[] args) {
Shape s1 = new Circle(); // reference type: Shape
Shape s2 = new Rectangle(); // reference type: Shape
s1.draw(); // JVM sees Circle → Circle.draw()
s2.draw(); // JVM sees Rectangle → Rectangle.draw()
}
}
Output:
Drawing a Circle
Drawing a Rectangle
The variable type is Shape in both cases, but the JVM looks at the real object (Circle and Rectangle) and dispatches accordingly — that is dynamic binding in action.
Tip: The
@Overrideannotation is your best friend here. It tells the compiler you intend to override a parent method, and the compiler will error if you accidentally mistype the name or parameters.
A Side-by-Side Comparison
class Parent {
static void staticMethod() {
System.out.println("Parent static");
}
void instanceMethod() {
System.out.println("Parent instance");
}
}
class Child extends Parent {
static void staticMethod() { // hides Parent.staticMethod
System.out.println("Child static");
}
@Override
void instanceMethod() { // overrides Parent.instanceMethod
System.out.println("Child instance");
}
}
public class BindingComparison {
public static void main(String[] args) {
Parent ref = new Child();
ref.staticMethod(); // static binding → Parent static
ref.instanceMethod(); // dynamic binding → Child instance
}
}
Output:
Parent static
Child instance
Same reference (ref), same object (Child), but the two method calls are resolved by completely different mechanisms.
Fields Are Always Statically Bound
Unlike methods, fields are always resolved at compile time based on the reference type, even when you override them in a subclass. This catches many developers off guard.
class Base {
String name = "Base";
}
class Derived extends Base {
String name = "Derived"; // hides Base.name — does NOT override
}
public class FieldBinding {
public static void main(String[] args) {
Base obj = new Derived();
System.out.println(obj.name); // static binding → Base
}
}
Output:
Base
Warning: Never rely on field hiding through polymorphic references. Keep fields
privateand expose them through getter methods so dynamic dispatch can take effect when needed.
Under the Hood
How Static Binding Works in Bytecode
When the compiler sees a private, static, or final method call, it emits one of these bytecode instructions:
invokestatic— forstaticmethodsinvokespecial— forprivatemethods, constructors, andsupercalls
Both instructions embed the exact class and method descriptor directly in the constant pool of the .class file. At runtime the JVM does a straightforward constant-pool lookup — essentially a direct address — with no additional searching.
How Dynamic Binding Works — The vtable
For ordinary (overridable) instance methods, the compiler emits invokevirtual. At class-loading time the JVM builds a virtual method table (vtable) for every class. Each entry is a pointer to the actual method body. When a subclass overrides a method, its vtable slot is updated to point to the subclass version.
At runtime, invokevirtual follows these steps:
- Look up the vtable for the actual object type (not the reference type).
- Index into the vtable at the slot for the called method.
- Jump to the method pointer stored there.
This is one indirection (pointer lookup) rather than a direct address — the JIT compiler often inlines the target method after it detects that only one concrete type is used at a call site, eliminating the overhead entirely in hot paths.
For interfaces the JVM uses invokeinterface, which involves a slightly more expensive search because a class can implement multiple interfaces. See vtable & Dynamic Dispatch for the complete deep-dive.
Note: The JIT’s inline cache and polymorphic inline cache (PIC) optimizations mean that in practice, dynamic dispatch is nearly as fast as static binding for the common case of one or two concrete types at a call site.
Quick Decision Guide
Use this checklist to predict which type of binding will occur:
- Is the method
private? → Static binding (invokespecial) - Is the method
static? → Static binding (invokestatic) - Is the method
final? → Static binding (invokespecial/ JIT-optimized) - Is it a field access? → Static binding (always)
- Is it a constructor call (
new/super())? → Static binding (invokespecial) - Everything else (overridable instance method)? → Dynamic binding (
invokevirtual/invokeinterface)
Related Topics
- Polymorphism — the big picture of many-forms in Java
- Runtime Polymorphism — dynamic binding in full detail with real-world examples
- Compile-Time Polymorphism — static binding through method overloading
- Method Overriding — the technique that makes dynamic binding useful
- vtable & Dynamic Dispatch — deep JVM internals behind dynamic binding
- final Keyword — how
finalmoves methods from dynamic to static binding