Skip to content
Java polymorphism 6 min read

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:

FeatureStatic BindingDynamic Binding
Resolved byCompilerJVM (at runtime)
Also calledEarly bindingLate binding
Methods involvedprivate, static, finalOverridden instance methods
PerformanceSlightly fasterTiny overhead (vtable lookup)
Enables polymorphism?NoYes

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:

  • private methods — not visible outside the class; can never be overridden.
  • static methods — belong to the class, not an instance; resolved by the declared type.
  • final methods — 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 static method through an object reference (like a.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 @Override annotation 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 private and 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 — for static methods
  • invokespecial — for private methods, constructors, and super calls

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:

  1. Look up the vtable for the actual object type (not the reference type).
  2. Index into the vtable at the slot for the called method.
  3. 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)
Last updated June 13, 2026
Was this helpful?