Topic 66 of 119 6. Object-Oriented Programming in Java Interactive Tutorial

Static vs Dynamic Binding in Java: Early vs Late Resolution

16 mins read Last updated: 22 Aug 2026 26,418 views

Direct Answer & Core Concept

Static binding fixes the target of a call or field access while compiling, from the types you wrote. Dynamic binding picks the target from the object that is actually there, while the program is running. Java uses both, and the compiler resolves almost everything before your code starts.
Main.java
Run Online
Sensor ref = new ThermalSensor();
System.out.println(ref.kind);
// "generic"           -> getfield
System.out.println(ref.read());
// "reading temperature" -> invokevirtual

Both lines read the same object, and they disagree, because they are bound differently. The field access compiles to getfield Sensor.kind, with the field named in the instruction, so the parent's slot is read no matter what the object really is. The method call compiles to invokevirtual Sensor.read, which fixes only the name; the receiver decides which implementation runs, and the child's version runs.

You can read this yourself rather than take it on trust. Compile the example and disassemble it:

Main.java
Run Online
javap -c -p out/BindingDemo.class

The instruction is the binding decision. invokestatic and getstatic name their target outright, which covers static methods, static fields, constructors, and explicit super calls. invokevirtual and invokeinterface fix the name and leave the receiver to be examined at run time. instanceof is a genuine run-time question about an object's type.

Three details correct the usual textbook account. A final method still compiles to invokevirtual, not a cheaper instruction; final removes the possibility of a subclass rather than changing the call site. A private method has emitted invokevirtual rather than invokespecial since Java 11. And a static final field holding a compile-time constant is not read at all: the value is copied into every caller as a literal, which is why changing such a constant forces a recompile of everything that uses it.

The performance story is usually told backwards. Late binding is not a slow reflective call, and marking a parameter final produces byte-for-byte identical bytecode. The JIT profiles each call site and inlines implementations behind type guards, even after seeing several receiver types. Reserve final for immutability and design guarantees, and profile before treating a virtual call as a bottleneck. When the method genuinely is not known until run time, that is reflection, and it does carry a real cost.

Detailed Explanation & Mechanics

1) Three Words That Are Not Synonyms

Static binding, early binding, and compile-time resolution are usually used interchangeably, and so are their dynamic, late, and runtime counterparts. That is mostly harmless, but it hides a distinction worth keeping: binding is about which method gets chosen, while resolution is about when the choice is made. The compiler resolves almost everything. The JVM resolves only the small remainder that genuinely could not be known earlier.

TermPrecise meaning
Static binding, early bindingThe target of a call or field access is fixed from declared types, and never varies at run time.
Dynamic binding, late bindingThe target is chosen from the actual object, so it can differ per object at run time.
ResolutionThe moment the compiler or JVM settles that target. Compilation for static binding, first execution for dynamic.
DispatchThe act of jumping to the chosen method, which happens on every call either way.

That last row is the one people get wrong. A static call is still dispatched on every invocation. What differs is not whether a jump occurs but whether the JVM had to look anything up to decide where to jump.

Earlier topics in this series cover the behaviour from the source side: polymorphism covers overloading versus overriding, and method overriding covers what makes a signature an override at all. This page looks at the same rules from the other end, by reading the bytecode that javac actually emits.

2) The Evidence: One Line, Six Instructions


Map of which Java operations are bound at compile time versus run time, showing invokestatic, getstatic, invokespecial and ldc on the early side, and invokevirtual, invokeinterface, getfield and instanceof on the late side
Which instruction settles the call, and who is left to decide the target.

Every call you write becomes one of a small number of instructions, and the choice of instruction is the binding decision. You can read it yourself with javap -c, the disassembler that ships with the JDK:

Main.java
Run Online
javap -c -p out/BindingDemo.class

Run that against the example at the end of this page and the whole topic becomes concrete. The output for main contains exactly these lines:

Main.java
Run Online
12: invokestatic #37 // Method Sensor.unit:()Ljava/lang/String;
12: getfield #16 // Field Sensor.kind:Ljava/lang/String;
30: invokevirtual #32 // Method Sensor.read:()Ljava/lang/String;
89: instanceof #7 // class ThermalSensor

Read them together and the rule is visible without any hand-waving. invokestatic names one method and that is the end of it. getfield names one field, and it is the one declared on the reference type. invokevirtual names a method but leaves the receiver to be examined when the call actually runs. instanceof is a genuine run-time question about an object's type.

3) The Full Taxonomy

ConstructInstructionBound
Static methodinvokestaticCompile time
Static field readgetstaticCompile time
Constructor callinvokespecialCompile time
Explicit super.m()invokespecialCompile time
Private methodinvokevirtual since Java 11Compile time, no override possible
Final instance methodinvokevirtualCompile time, no override possible
Ordinary instance methodinvokevirtualRun time
Call through an interfaceinvokeinterfaceRun time
Instance field readgetfieldCompile time, from the reference type
Type testinstanceofRun time

Two rows deserve a second look, because both contradict widely repeated advice.

A final method is still invokevirtual. Marking a method final does not change the instruction at all. It removes the possibility of a subclass replacing the method, which is a real guarantee, but the call site is byte-for-byte the same as for a non-final one:

Main.java
Run Online
final String calibration() {
    ...
} // javap: invokevirtual Sensor.calibration
String read() {
    ...
} // javap: invokevirtual Sensor.read

A private method is invokevirtual too, since Java 11. Earlier releases emitted invokespecial here. Because a private method cannot be inherited, no override is possible, so the effect is static either way, but the instruction changed and older blog posts still show the old form.

4) Fields Bind to the Reference, Not the Object

This is the single most reliable way to tell early from late binding in practice, and the polymorphism page already showed the behaviour. What the bytecode adds is the reason.

Main.java
Run Online
class Sensor {
    protected String kind = "generic";
    public String read() {
        return "reading generic";
    }
}final class ThermalSensor extends Sensor {
    protected String kind = "thermal";
    // HIDES the parent field
    @Override public String read() {
        return "reading temperature";
    }
}Sensor ref = new ThermalSensor();
System.out.println(ref.kind);
// "generic"          -> getfield Sensor.kind
System.out.println(ref.read());
// "reading temperature" -> invokevirtual

The two fields are separate storage slots in one object. ref.kind compiles to getfield Sensor.kind, with the field named in the instruction, so the parent's slot is read no matter what the object really is. ref.read() names only the method; the receiver decides which implementation runs.

This is why fields are never polymorphic in Java. Hiding is not overriding, and the compiler will not silently merge two same-named fields.

5) Early Binding You Can See: Constants Are Inlined

The clearest demonstration of early binding is that some field references disappear from the bytecode altogether. A static final field initialised with a compile-time constant is not a field access at run time; it is a literal baked into the calling code.

Main.java
Run Online
class Config {
    static final int MAX = 10;
    static final String TAG = "base";
}System.out.println(Config.MAX);
System.out.println(Config.TAG);
System.out.println("a" + "b");

Disassemble the caller and none of those three lines touch a field:

Main.java
Run Online
13: bipush 10 // MAX was folded into a literal
20: ldc #28 // String base       // TAG was folded into a constant
26: ldc #46 // String concat: ab // concatenation folded too

That is the strongest form of static binding there is. There is no lookup to perform, because the answer was decided when the class was compiled and copied into every caller.

The rule is narrow but worth stating precisely: the field must be static final and the initialiser must be a constant expression. A non-final field, or one built at run time, compiles to a real getstatic. The same folding applies to compile-time string concatenation, which is why "a" + "b" becomes one constant while "a" + name does not.

A corollary catches people out. This field:

Main.java
Run Online
protected final String kind = "generic";

is a constant variable, so reading ref.kind emits ldc and never emits getfield at all. To demonstrate field binding honestly you need a non-final field, which is why the example on this page declares kind without final.

This folding is also why public static final constants are a mild design smell in a mutable-programming sense: the value is copied into every class that reads it, so changing it requires recompiling all of them.

6) The Performance Claim Is Usually Wrong

You will hear early binding described as faster because it allows inlining, and late binding as slower because of the lookup. Half of that is right, and the half that is wrong causes real design damage.

What is true: an invokestatic target is fixed in the instruction, so the JIT never has to ask which method to run. What is false: an invokevirtual is not a slow reflective call. Modern JIT compilers profile each call site, notice which receiver types actually arrive, and inline speculatively behind a type guard.

The demonstration is worth doing, because it is not what most people expect. Here are two methods that differ only in whether the parameter type is final:

Main.java
Run Online
static int via(A a) {
    return a.f();
}static int viaFinal(final A a) {
    return a.f();
}

Disassemble them and they are identical, instruction for instruction:

Main.java
Run Online
static int via(J$A);
0: aload_0 1: invokevirtual #7 // Method J$A.f:()I
4: ireturnstatic int viaFinal(J$A);
0: aload_0 1: invokevirtual #7 // Method J$A.f:()I
4: ireturn

Then run with -XX:+PrintInlining and let the call site see three different receiver types. The JIT first reports "no static binding" while it profiles, and afterwards inlines all three implementations, each behind its own guard:

Main.java
Run Online
@ 40 J::via(5 bytes) inline @ 1 J$A::f(2 bytes) inline@ 44 J::via(5 bytes) inline @ 1 J$B::f(2 bytes) inline@ 49 J::via(5 bytes) inline @ 1 J$C::f(2 bytes) inline

So a call site that is genuinely megamorphic still gets inlined, several ways, at run time. Marking a method final does still help, but for a different reason than the folklore suggests: it removes the possibility of a subclass, which lets the JIT devirtualize without a guard. Marking a parameter final does nothing at all, because it never changed the instruction.

Do not add final to methods or parameters purely for speed. Use it where immutability or a design guarantee is the actual goal. Reach for performance measurements, and specifically for a profiler, before concluding that a virtual call is your bottleneck.

7) Where Late Binding Wins: Reflection

Everything above is decided before the program runs, which raises an obvious question: how do frameworks like Spring, Jackson, or JUnit choose a method when the class is not known until later? The answer is that they abandon bytecode dispatch and use the reflection API, which looks methods up by name at run time.

Main.java
Run Online
Method m = Sensor.class.getMethod("read");
Object result = m.invoke(new ThermalSensor());
// "reading temperature"

That is the most extreme form of late binding available, and it carries the cost the JIT cannot optimise away: the method is found by string comparison, access checks are made on every call, and the JIT sees far less than it does for a direct call. It is the right tool when the name is genuinely not known until run time, and the wrong tool for everything else.

8) Common Misconceptions

ClaimVerdict
Late binding means a slow reflective callFalse. It is invokevirtual, and the JIT inlines it behind a guard.
Marking a parameter final speeds up the callFalse. The bytecode is identical.
A final method gets a different instructionFalse. Still invokevirtual.
Private methods always use invokespecialOutdated. Since Java 11 they use invokevirtual.
Fields dispatch on the actual objectFalse.getfield uses the reference type.
A final String field is read with getfieldFalse. It is a constant and gets folded.
Overloading is late bindingFalse. The compiler picks from declared types.
Static binding means the method is not dispatchedFalse. It is still called; the target is just already known.

9) How to Check This Yourself

None of this needs to be taken on trust. Three commands will confirm any claim on this page against your own code:

Main.java
Run Online
javac -d out BindingDemo.java # compile as normaljavap -c -p out/BindingDemo.class # read the instructionsjava -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining -cp out BindingDemo

The first prints bytecode with the comment column naming the exact field or method each instruction refers to. The second shows what the JIT decided at run time. Reading those two outputs side by side is the fastest way to internalise the whole topic, because you are looking at the real decisions rather than a diagram of them.

One caution when experimenting: a debugger that shows a value for a field may be reading the other field from the same object, since both slots exist. Trust the bytecode over what a variable inspector displays.

Frequently Asked Questions

What is the difference between static and dynamic binding in Java?


Static binding fixes the target of a call from the declared types while compiling, so the same target is used for every object. Dynamic binding picks the target from the actual object at run time, which is what makes an overridden method run. In bytecode the distinction shows up directly: static calls compile to invokestatic or getstatic with the target named in the instruction, while dynamic calls compile to invokevirtual or invokeinterface with only the name fixed.

Which methods and fields use static binding in Java?


Static methods, static field accesses, constructors, and explicit super calls are settled at compile time. Private and final instance methods are also effectively static because no subclass can replace them, even though they compile to invokevirtual like any other instance method. Instance field reads are static too, and are settled from the reference type rather than the object.

Is late binding slower than early binding?


Not in any way you should design around. A late-bound call is an invokevirtual instruction, not a reflective call, and modern JIT compilers profile the call site and inline the implementation behind a type guard, even when several receiver types have been observed. Marking a parameter final does not change the bytecode at all, so it cannot help. Profile before changing a design for performance.

Why do instance fields not dispatch on the actual object?


Because a field access compiles to getfield with the field named in the instruction, taken from the reference type. If a subclass declares a field with the same name, both fields exist as separate slots in the object and the compiler will not merge them. This is why fields are never polymorphic in Java and why reading a shadowed field through a supertype reference returns the parent's value.

Does a final method use a different bytecode instruction?


No. A final instance method compiles to invokevirtual, exactly like a non-final one. What final guarantees is that no subclass can replace it, which lets the JIT devirtualize the call without a guard. The instruction itself is identical, so the benefit is a stronger guarantee rather than a cheaper call site.

Do private methods still use invokespecial?


Not since Java 11. Older compilers emitted invokespecial for a call to a private method, and a great deal of online material still shows that. Since Java 11 they emit invokevirtual, because the change to private interface methods made that necessary. The behaviour is unchanged either way, since a private method cannot be inherited and so can never be overridden.

Are static final constants read from the field at run time?


Usually not at all. When a static final field is initialised with a compile-time constant, javac copies the value into every class that uses it and emits a literal or ldc rather than a getstatic. Compile-time string concatenation is folded the same way. This is why changing a public static final constant requires recompiling everything that reads it.

Is overloading static binding or dynamic binding?


Static. The compiler selects the overload from the declared types of the arguments before the program runs, and that choice is baked into the instruction. If no overload matches, it is a compile error rather than a decision deferred to run time. Overriding is the dynamic case.

When is real late binding actually needed?


When the method to call is not known until run time, which in practice means reflection. Frameworks use it to discover annotated methods, invoke handlers by name, and instantiate types from configuration. That is the genuine cost of late binding, and it is worth paying only when the alternative is recompiling code that already knows the name.

Runnable Code Example: Static vs Dynamic Binding in Java

Java 17+
Main.java
Run Online
// Shows which part of a call is settled by javac and which part is left
// to the JVM. Only the line marked LATE depends on the actual object.
class Sensor {
    // An ordinary instance field, NOT final. Reading it emits getfield and
    // the reference type decides which field is read, so this prints
    // "generic" even though the object is a ThermalSensor.
    protected String kind = "generic";
    // A static method. Settled from the DECLARING class, so calling it
    // through a reference does not compile at all.
    static String unit() {
        return "raw";
    } // A private method. Not inherited, so no child can replace it.
    private String secret() {
        return "base-secret";
    } // A final method. No child can replace it, although the bytecode is
    // still invokevirtual.
    final String calibration() {
        return "factory-default";
    } // An ordinary instance method. This one is left to the JVM.
    public String read() {
        return "reading generic";
    } public String exposeSecret() {
        return secret();
    }
}final class ThermalSensor extends Sensor {
    // Hides the parent field. Both fields exist in the same object.
    protected String kind = "thermal";
    @Override public String read() {
        return "reading temperature";
        // LATE: chosen from the object
    }
}public class BindingDemo {
    public static void main(String[] args) {
        Sensor ref = new ThermalSensor();
        // reference type decides fields
        System.out.println("field   = " + ref.kind);
        System.out.println("method  = " + ref.read());
        System.out.println("static  = " + Sensor.unit());
        System.out.println("private = " + ref.exposeSecret());
        System.out.println("final   = " + ref.calibration());
        System.out.println("instanceof = " +(ref instanceof ThermalSensor));
    }
}// Actual output:
// field   = generic
// method  = reading temperature
// static  = raw
// private = base-secret
// final   = factory-default
// instanceof = true

Common Java Technical Interview Questions & Answers

Interview Prep
Q1

What is the difference between static and dynamic binding in Java?

Static binding fixes the target from declared types while compiling, so every object uses the same target. Dynamic binding picks the target from the actual object at run time, which is what makes an overridden method run. In bytecode the distinction is direct: static calls emit invokestatic or getstatic with the target named in the instruction, while dynamic calls emit invokevirtual or invokeinterface with only the name fixed.
Q2

Which methods and fields are bound statically?

Static methods, static field reads, constructors, and explicit super calls. Private and final instance methods are also effectively static because no subclass can replace them, although they still emit invokevirtual. Instance field reads are static as well, settled from the reference type rather than the actual object, which is why a field hidden by a subclass does not shadow it for dispatch purposes.
Q3

Is dynamic binding slower than static binding?

Not in a way that should influence a design. A dynamically bound call is an invokevirtual instruction rather than a reflective call, and the JIT profiles the call site and inlines the implementation behind a type guard even after observing several receiver types. Marking a parameter final leaves the bytecode identical, so the old advice to do so for speed has no basis.
Q4

Does a final method compile to a different instruction?

No. A final instance method emits invokevirtual exactly like a non-final one; final changes what the compiler permits, not what it emits. What it buys is the guarantee that no subclass can replace the method, which lets the JIT devirtualize the call without a guard. That is a real benefit, but it is a design guarantee rather than a different instruction.
Q5

Why do private methods emit invokevirtual now?

Since Java 11, and the change came with private interface methods. Older compilers emitted invokespecial for a private call, and much published material still shows that. Because a private method cannot be inherited it can never be overridden, so the binding was always effectively static; only the instruction encoding moved.
Q6

Are static final constants read at run time?

Usually not. When a static final field is initialised with a constant expression, javac copies the value into each calling class and emits a literal or ldc instead of a getstatic, and compile-time string concatenation is folded the same way. The consequence is that changing such a constant requires recompiling everything that reads it, which is worth remembering before exposing one widely.
Q7

Why does a field read ignore the object's real type?

A field access compiles to getfield naming the field, and the compiler takes that name from the reference type. A subclass field of the same name is a separate slot in the object, not a replacement, so both exist and the parent's is the one read. This is why fields are not polymorphic in Java and why hiding a field is not overriding it.
Q8

Is method overloading static or dynamic binding?

Static. The compiler selects the overload from the declared argument types before the program runs, and the chosen signature is baked into the instruction. A call that matches no overload is a compile error rather than a decision postponed to run time. Overriding is the dynamic counterpart.
Q9

When is genuinely late binding justified?

When the method to invoke is not known until run time, which in practice means reflection. Frameworks use it to find annotated methods, invoke handlers by name, and build types from configuration. That path resolves by string comparison and re-checks access on every call, so it is worth paying for only when the alternative is recompiling code that already knows the name.
Q10

How can I verify a binding claim myself?

Compile the class and run javap -c -p on it. The comment column names the exact field or method each instruction refers to, so you can see whether a call site names its target or leaves it open. For the run-time half, add -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining and see what the JIT actually decided. Reading both outputs side by side settles most arguments about performance.

Key Takeaways, Best Practices & Clean Code Rules

Java Best Practice & Production Rules

Never add final to a parameter or method purely for speed, it changes no bytecode.
Use final where it expresses immutability or a real design guarantee instead.
Remember a static final constant is copied into every caller at compile time.
Do not expect a subclass field to shadow a parent field for dispatch purposes.
Type parameters and locals as the supertype so methods dispatch on the real object.
Reach for an interface or abstract class when you want a family to share a contract.
Keep static methods for genuine per-type utilities, never for hidden mutable state.
Prefer getters over public fields so the binding rules stay predictable and the API can evolve.
Remember private and final methods are unbound in practice but still emit invokevirtual.
Use javap -c on any code whose binding behaviour you are unsure about.
Profile before blaming a virtual call, the JIT inlines them behind type guards.
Prefer reflection only when the target really is unknown until run time.
Remember overload selection happens at compile time and is never revisited.
Use final fields to make objects safely publishable across threads.
Do not use -XX:+PrintInlining as a performance measurement, only as a JIT decision trace.
We value your privacy & transparency

OperateTools uses cookies and local storage to ensure basic tool functionality, preserve preferences, and analyze aggregated traffic. We never sell your personal data or index your document files. Learn more in our Privacy Policy.