Back to Blog
Java

Java Compile Time Binding: Static Method Resolution

Java compile-time binding explained: 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 the method call based on the declared type of the reference variable, not the actual object type. For static and private methods, the compiler can emit an invocation that identifies the target method without virtual dispatch. The JVM does not need to inspect the runtime object to choose among overrides.

Which Method Decisions Happen at Compile Time

Compile-time binding applies to method calls that cannot be overridden or to decisions that the compiler makes 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. When the call resolves to the final declaration, the implementation cannot be changed by a subclass.
  • Overload selection: When multiple methods share the same name but differ in parameters, the compiler selects the most specific applicable version at compile time. Overload selection is separate from the runtime dispatch of the selected method, which may still be virtual if the method is an overridable instance method.

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 overload decisions happen at compile time, so the JVM does not have to choose between the two overloads at runtime.

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 and private methods, final method declarations, overload selectionOverridden instance methods
Dispatch modelNo override lookupinvokevirtual or invokeinterface
PerformanceNo dynamic dispatch overhead for most callsSmall 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 can avoid dynamic dispatch overhead, especially for static and private methods. Because the compiler has already selected the target, the JVM does not need to search for an override at the call site. Modern JVMs optimize virtual dispatch heavily, however, 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

Final methods are not overridable in subclasses, so a call that resolves to the final declaration is compile-time bound. The classic caveat is a final method used as the implementation of an interface method. If the declared type is the interface, the compiler sees the interface method, not the final class method, and the call compiles to interface dispatch. The JVM selects the final implementation at runtime. The implementation still cannot be overridden by a subclass, but the dispatch path is not a compile-time final-method resolution.

Another edge case involves method references and lambda expressions. A method reference such as this::someMethod is type-checked against the target functional interface at compile time. At the invocation site, however, the referenced method still follows the normal dispatch rules: if it is an overridable instance method, the JVM dispatches it based on the receiver's runtime type.

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: Static Resolution vs Runtime Dispatch | RYUSLOG DEV