Back to Blog
Java

Java Compile Time Binding: Static Method Resolution

java compile time binding: Explains how Java resolves method calls at compile time, which methods use static binding, and how it differs from runtime dispatch.

Javastatic bindingmethod overloadingJVM dispatchmethod resolution
Diagram showing compile-time method resolution in Java with a reference variable pointing to a resolved static method.

When a Java compiler sees a method call, it must decide which method implementation the call refers to. For some calls, that decision happens entirely at compile time. This is called java compile time binding, also known as static binding. For others, the decision is deferred to the JVM at runtime.

Compile-time binding means the compiler resolves a method invocation based on the declared type of the reference variable, not the actual object type. The bytecode generated for such a call contains a direct reference to the resolved method. The JVM does not need to inspect the runtime object to determine which implementation to execute.

Which Methods Use Compile-Time Binding

Java uses compile-time binding for method calls that cannot be overridden or that are resolved by the compiler without needing runtime type information. These include:

  • Static methods: A static method belongs to the class itself, not to an instance. The compiler resolves the call using the declared type of the reference.
  • Private methods: Private methods are not visible to subclasses, so they cannot be overridden. The compiler resolves them directly.
  • Final methods: A final method cannot be overridden in a subclass. The compiler knows the implementation will not change.
  • Overloaded methods: When multiple methods share the same name but differ in parameters, the compiler selects the most specific applicable version at compile time.

Consider this example:

public class Calculator { public static int add(int a, int b) { return a + b; } public static double add(double a, double b) { return a + b; } }

When you call Calculator.add(3, 4), the compiler selects the int version because the arguments are int. When you call Calculator.add(3.0, 4.0), it selects the double version. Both decisions happen at compile time, and the bytecode contains direct invocations to the selected methods.

How the Compiler Resolves the Call

The resolution process for compile-time binding follows a specific order. The compiler first identifies the class or interface that declares the method, using the static type of the reference. It then searches for a method with a matching name and compatible parameter types.

When multiple overloads are applicable, the compiler applies the most specific method rule. It selects the version whose parameter types are the closest match to the arguments. This is why add(3, 4) selects the int version over a long or double version when both exist.

The declared type of the reference variable is what matters. If you have:

Object obj = new String("hello");

The compiler sees obj as Object. Any method call on obj is resolved using the Object type. If Object does not declare the method you are calling, the code does not compile, even if the runtime object is a String.

Compile-Time Binding vs Runtime Binding

Runtime binding, also called dynamic dispatch, applies to overridden instance methods. When you call a non-static, non-final, non-private method on a reference, the JVM decides at runtime which implementation to execute based on the actual object type.

AspectCompile-Time BindingRuntime Binding
Resolution timeCompile timeRuntime (JVM dispatch)
Based onDeclared reference typeActual object type
Applies toStatic, private, final, overloaded methodsOverridden instance methods
BytecodeDirect method referenceinvokevirtual or invokeinterface
PerformanceNo dispatch overheadSmall dispatch overhead

The distinction matters when a subclass overrides a method. Consider:

class Animal { void speak() { System.out.println("Animal speaks"); } static void describe() { System.out.println("An animal"); } } class Dog extends Animal { @Override void speak() { System.out.println("Dog barks"); } static void describe() { System.out.println("A dog"); } }

If you write:

Animal a = new Dog(); a.speak(); // runtime binding: prints "Dog barks" a.describe(); // compile-time binding: prints "An animal"

The speak() call uses runtime binding because it is an overridden instance method. The JVM looks at the actual object type, which is Dog, and invokes Dog.speak(). The describe() call uses compile-time binding because it is a static method. The compiler resolves it using the declared type Animal, so Animal.describe() runs.

Overloading and Compile-Time Binding

Overloading is a common source of confusion because it interacts with both compile-time and runtime binding. The overload resolution itself happens at compile time. The compiler selects which method signature to call based on the static types of the arguments. Once selected, the invocation may still use runtime binding if the selected method is an overridable instance method.

This means the argument types determine the method signature, but the actual object type determines the implementation. A common mistake is assuming that the runtime type of an argument influences overload selection. It does not. The compiler only sees the declared type.

public class Printer { public void print(String value) { System.out.println("String: " + value); } public void print(Object value) { System.out.println("Object: " + value); } } Object obj = "text"; Printer p = new Printer(); p.print(obj); // compile-time binding selects print(Object)

Even though obj holds a String at runtime, the compiler selects print(Object) because the declared type of obj is Object. This is a frequent source of bugs when developers expect runtime type-based dispatch for overloaded methods.

Performance and JVM Behavior

Compile-time binding has a performance advantage over runtime binding because the JVM does not need to perform virtual method dispatch. The bytecode contains a direct method reference, and the JVM can resolve it quickly. However, modern JVMs optimize virtual dispatch heavily, so the practical difference is usually negligible for most applications.

The more important operational concern is correctness. When you rely on compile-time binding for static methods, you must remember that the declared type of the reference controls which method runs. This can lead to surprising behavior when a subclass defines a static method with the same signature as its parent. The subclass method does not override the parent method; it hides it. The compiler selects the version based on the declared type of the reference.

This hiding behavior is a common source of maintenance bugs. If a developer later changes the declared type of a variable, the static method that gets called can change even though the runtime object stays the same. Code reviews should check that static method calls on polymorphic references are intentional.

Edge Cases and Common Misconceptions

One misconception is that final methods always use compile-time binding. A final method cannot be overridden, so the compiler can resolve it directly. However, if the method is invoked through an interface reference, the JVM may still use an interface dispatch mechanism. The practical effect is the same: the implementation is known, but the bytecode may differ depending on how the call is expressed.

Another edge case involves method references and lambda expressions. When you write this::someMethod, the compiler resolves the method reference at compile time. The target method is selected based on the functional interface's signature. This is compile-time resolution, but the actual invocation may still use runtime binding if the referenced method is an overridable instance method.

The separation between method resolution and implementation selection is what makes compile-time binding predictable. When you call a static method on a polymorphic reference, the compiler uses the declared type. When you call an overridable instance method, the JVM uses the actual object type. Keeping these two rules in mind prevents the most common binding-related bugs in Java code.

java compile time binding: Practical Usage and Code Examples | RYUSLOG DEV