Method Resolution

When several overloaded or overridden methods could match a call, Java resolves which one runs using a strict, staged compile-time process — plus one dynamic step for overriding.

Given a method call, Java has to decide which actual method body runs. Overload resolution (choosing among methods with the same name but different parameter lists) happens entirely at compile time, in three strict phases; overriding (a subclass replacing an inherited method's implementation) is resolved at compile time for which signature applies but dispatches to the correct implementation at runtime based on the object's actual class.

Overloading vs Overriding — Two Different Mechanisms

Overloading means multiple methods share a name but differ in parameter list (type, number, or order of parameters) within the same class or class hierarchy — which overload is called is decided entirely at compile time, based on the static (declared) types of the arguments. Overriding means a subclass provides its own implementation of an inherited instance method with the identical signature — which implementation actually runs is decided at runtime, based on the object's actual runtime class (dynamic dispatch).

This distinction — overload resolution is static, override dispatch is dynamic — is one of the most consequential rules in the language and a frequent source of surprises.

Compile-Time Overload Resolution, Phase by Phase

The compiler resolves an overloaded call in up to three phases, using the FIRST phase where exactly one applicable method is found (stopping there — it does not consider later, looser phases once an earlier phase succeeds): Phase 1, strict invocation (no boxing/unboxing, no varargs — only subtyping/widening reference/primitive conversions); Phase 2, loose invocation (boxing/unboxing allowed, still no varargs); Phase 3, variable-arity invocation (varargs methods considered).

static void call(long x) { System.out.println("long"); }
static void call(Integer x) { System.out.println("Integer"); }
static void call(int... x) { System.out.println("varargs"); }

call(5); // "long" - Phase 1 (int widens to long without boxing) wins before boxing/varargs are even considered

Most-Specific Method Selection

If a phase finds more than one applicable method, the compiler picks the most specific one — informally, the one whose parameter types are all assignable to every other applicable method's corresponding parameter types. If no single method is more specific than all the others, the call is ambiguous and is a compile-time error.

static void show(Object o) { System.out.println("Object"); }
static void show(String s) { System.out.println("String"); }

show("hi"); // "String" - String is more specific than Object for this argument

Boxing/Unboxing in Overload Resolution

Because boxing/unboxing is only considered in Phase 2 (after Phase 1 fails using pure widening), a widening primitive conversion is always preferred over an autoboxing conversion when both would otherwise apply.

static void pick(long x) { System.out.println("long"); }
static void pick(Integer x) { System.out.println("Integer"); }

pick(5); // "long": widening int->long (Phase 1) beats boxing int->Integer (Phase 2)

Varargs Come Last

A varargs method is only chosen if no fixed-arity method matches in Phase 1 or Phase 2 — varargs is deliberately the last resort, both because it's the least type-restrictive form and to avoid varargs methods silently stealing calls meant for a more specific fixed-arity overload.

static void log(String msg) { System.out.println("one-arg"); }
static void log(String... msgs) { System.out.println("varargs"); }

log("hello"); // "one-arg" - fixed-arity overload wins over varargs even though both apply

Overload Resolution Uses Static (Declared) Types

Which overload is selected depends on the compile-time declared type of each argument expression, not its runtime type. This is a common source of confusion when a reference variable's declared type differs from the actual object's class.

static void print(Object o) { System.out.println("Object version"); }
static void print(String s) { System.out.println("String version"); }

Object value = "actually a string";
print(value); // "Object version" - the compiler only sees value's DECLARED type, Object

Overriding and Dynamic Dispatch

For instance methods, once the compiler has determined WHICH signature is being invoked (by overload resolution on static types), the JVM decides WHICH implementation runs at runtime based on the actual runtime class of the receiver object — this is dynamic (virtual) dispatch, and it's what makes polymorphism work.

class Animal { String speak() { return "..."; } }
class Dog extends Animal { @Override String speak() { return "Woof"; } }

Animal a = new Dog();
System.out.println(a.speak()); // "Woof" - runtime type (Dog) decides which override runs

Static Methods, Fields, and Private Methods Are Not Overridden

Only instance methods participate in dynamic dispatch/overriding. Static methods can be hidden (a subclass can declare a static method with the same signature, but which one runs is decided by the reference's declared/static type, not the runtime object) — not overridden. Fields are never polymorphic at all; field access always uses the declared/static type of the reference.

Calling a static method through an instance reference (b.label()) is legal but misleading style; both the field and static-method access here are resolved statically, unlike speak() in the earlier example.

class Base {
    static String label() { return "Base"; }
    String field = "base-field";
}
class Derived extends Base {
    static String label() { return "Derived"; } // hides, doesn't override
    String field = "derived-field"; // hides, doesn't override
}

Base b = new Derived();
System.out.println(b.label());  // "Base" - static methods resolved by declared type
System.out.println(b.field);    // "base-field" - fields resolved by declared type

Rules for a Valid Override

A subclass method overrides a superclass method only if it has the same name, the same parameter types (erasure-compatible for generics), a covariant (same or narrower subtype) return type, and does not narrow accessibility or throw new/broader checked exceptions than the overridden method. Getting any of these slightly wrong (e.g. a different parameter type) accidentally creates a new overload instead of an override.

Always use @Override when the intent is to override — it turns an accidental overload (a real, common bug) into a compile-time error.

class Base {
    void process(List<String> items) { }
}
class Sub extends Base {
    @Override
    void process(ArrayList<String> items) { } // compile error!
    // @Override forces the compiler to verify this is really an override -
    // ArrayList<String> is not the same parameter type as List<String>,
    // so this would silently be an overload without @Override, not an override.
}

Compile-Time vs Runtime Selection — Summary

Putting it together: overload resolution (which method signature is called) is entirely a compile-time decision based on declared/static argument types. Dynamic dispatch (which implementation of that signature actually executes) is a runtime decision based on the receiver's actual class — but it only applies to overridden instance methods; static methods and fields never participate in it.

Edge Cases

Passing null to overloaded methods with unrelated reference-type parameters (e.g. foo(String) and foo(Integer)) is ambiguous and a compile-time error unless one parameter type is more specific than the others (e.g. foo(String) vs foo(Object) — foo(null) picks foo(String), the more specific one).

Generic methods interact with overload resolution using their erased/inferred types for applicability, which can occasionally produce a result that looks surprising without tracing through all three resolution phases.

Mental Model

Think of overload resolution as the compiler picking which door (method signature) to walk through, using only what it can see written on the labels at compile time (declared types) — it tries the strictest matching rule first, then relaxes (allowing boxing, then varargs) only if nothing strict-enough matches. Once through the chosen door, dynamic dispatch is a separate question the JVM answers at runtime: given the object that's actually there, which class's version of that door's implementation actually runs — and only overridable instance methods get asked that second question at all.

Quick Reference

Overload resolution (compile time), in order:
  Phase 1: strict - subtyping/widening only, no boxing, no varargs
  Phase 2: loose  - boxing/unboxing allowed, no varargs
  Phase 3: variable arity - varargs methods considered
  Ties within a phase -> most-specific method wins, else ambiguous (compile error)

Override dispatch (runtime): only for instance methods, based on receiver's actual class
NOT dynamically dispatched: static methods (hidden, resolved by declared type), fields (declared type)

Key Takeaways

Overload resolution happens entirely at compile time, in three ordered phases: strict, then boxing-allowed, then varargs — stopping at the first phase with exactly one applicable, most-specific method.

Which overload is chosen depends on the arguments' declared (static) types, not their runtime types.

Overriding an instance method is resolved dynamically at runtime, based on the receiver's actual class — this is the mechanism behind polymorphism.

Static methods are hidden, not overridden, and fields are never polymorphic — both resolve by the reference's declared type, not the runtime object.

Always annotate intended overrides with @Override so the compiler catches an accidental overload (mismatched signature) as an error instead of silently creating a new method.

Object value = "hello"; print(value); // expecting the String overload to run

String value = "hello"; print(value); // now the String overload is correctly selected // or: print((String) value); // explicit cast if the declared type can't change

Overload resolution uses the argument's declared (static) type, not its runtime type; declaring value as Object means the compiler can only see 'Object', so it selects the Object overload regardless of what's actually stored at runtime.

class Base { void handle(List<String> items) { } } class Sub extends Base { void handle(ArrayList<String> items) { } // no @Override: silently a NEW overload, not an override class Sub extends Base { @Override void handle(List<String> items) { } // @Override forces exact signature match }

Without @Override, a parameter-type mismatch (ArrayList<String> vs List<String>) creates an unrelated overloaded method instead of overriding the intended one, silently breaking polymorphism; @Override turns this mistake into a compile-time error.

class Base { static String id() { return "Base"; } } class Derived extends Base { static String id() { return "Derived"; } } Base ref = new Derived(); System.out.println(ref.id()); // assuming polymorphism applies here

System.out.println(Derived.id()); // call static methods on the class, not an instance reference, // to avoid implying they're polymorphic

Static methods are hidden, not overridden — calling ref.id() through a Base-typed reference resolves to Base.id() regardless of the object's actual runtime class, which is easy to misread as dynamic dispatch.

What are the three phases the compiler uses for overload resolution, and in what order are they tried?

Phase 1 (strict: only subtyping/widening conversions, no boxing, no varargs), Phase 2 (loose: boxing/unboxing allowed, still no varargs), Phase 3 (variable-arity: varargs methods considered). The compiler stops at the first phase where exactly one most-specific applicable method exists.

Why does call(5) prefer a long overload over an Integer overload, given both int-to-long widening and int-to-Integer boxing could apply?

Widening primitive conversion is checked in Phase 1, before boxing is even considered (Phase 2 only runs if Phase 1 finds no match), so the long overload is selected first and boxing is never evaluated.

What decides which overload is called when an argument's declared type differs from its runtime type (e.g. an Object-typed variable actually holding a String)?

Overload resolution uses the argument expression's compile-time declared (static) type only — an Object-declared variable selects the Object overload even if it happens to reference a String object at runtime.

How does overriding differ from overloading in terms of when the correct method is chosen?

Overload resolution (which signature is invoked) is decided entirely at compile time from declared types. Overriding additionally involves runtime dynamic dispatch: once the signature is fixed at compile time, the JVM chooses which class's implementation of that signature to run based on the object's actual runtime class.

Are static methods dynamically dispatched like instance methods? What term describes what a subclass does when it declares a static method with the same signature as its superclass?

No — static methods are resolved by the reference variable's declared type, not the runtime object, so they are not dynamically dispatched. When a subclass declares a static method with the same signature, it hides the superclass's version rather than overriding it.

Why is it good practice to always use @Override when intending to override a method?

Because a small signature mismatch (wrong parameter type, missing/extra parameter, etc.) silently creates an unrelated overload instead of the intended override, breaking polymorphism without any compiler warning. @Override forces the compiler to verify the method really does override a superclass/interface method, turning that mistake into a compile-time error.

Why is a varargs method only chosen as a last resort in overload resolution?

Varargs (Phase 3) is only considered if no method matches in Phase 1 (strict) or Phase 2 (loose, boxing allowed); this ordering prevents a broader, more permissive varargs overload from stealing a call that a more specific fixed-arity method could handle.

What does each call to f print, and why, tracing through the overload resolution phases?

f(5) prints "int": the literal 5 is already an int, so f(int) matches exactly in Phase 1 (no conversion needed at all), beating f(long) (needs widening), f(Object) (needs boxing), and f(int...) (varargs, last resort). f(s) with short s prints "long": short cannot widen directly to int as an exact match candidate here since f(int) requires... actually short DOES widen to int via a primitive widening conversion, so f(int) is also a Phase-1 candidate. Both f(int) and f(long) are reachable by widening from short in Phase 1; f(int) is more specific (short->int is a narrower widening step than short->long), so f(int) wins and it prints "int" — the key teaching point is that among multiple Phase-1-applicable methods, the most specific one is chosen, not the first one tried.

Write a Base/Sub example demonstrating that field access is NOT polymorphic while method overriding IS, printed side by side.

public class FieldVsMethodDemo { static class Base { String label = "Base-field"; String describe() { return "Base-method"; } } static class Sub extends Base { String label = "Sub-field"; // hides Base.label, does not override (fields aren't polymorphic) @Override String describe() { return "Sub-method"; } // genuinely overrides } public static void main(String[] args) { Base ref = new Sub(); System.out.println(ref.label); // "Base-field" - resolved by declared type (Base) System.out.println(ref.describe()); // "Sub-method" - resolved by runtime type (Sub) } } ref.label prints "Base-field" because field access is always resolved using the reference's declared (static) type — here Base — regardless of the object's actual class; Sub.label simply hides, rather than overrides, Base.label. ref.describe() prints "Sub-method" because instance method calls use dynamic dispatch: the JVM looks at the object's actual runtime class (Sub) to decide which override to run, which is exactly what makes overriding useful for polymorphism, unlike field access.

Explain, referencing the three-phase resolution process, why void m(String s) and void m(Object o) overloads never cause ambiguity for a String argument, but two same-phase-applicable methods with unrelated parameter types (e.g. m(String) and m(StringBuilder) called with null) do cause ambiguity.

For m("text"): both m(String) and m(Object) are applicable in Phase 1 (String needs no conversion; Object needs an upcast/widening reference conversion). Among Phase-1-applicable methods, the compiler picks the most specific one — String is a subtype of Object, so every argument valid for m(String) is also valid for m(Object) but not vice versa, making m(String) strictly more specific; no ambiguity. For m(null) with m(String) and m(StringBuilder): both are Phase-1 applicable (null is assignable to any reference type), but String and StringBuilder are unrelated types — neither parameter type is a subtype of the other, so neither method is more specific than the other, and the call is genuinely ambiguous, producing a compile-time error.

static void f(int x) { System.out.println("int"); }
static void f(long x) { System.out.println("long"); }
static void f(Object x) { System.out.println("Object"); }
static void f(int... x) { System.out.println("varargs"); }

public static void main(String[] args) {
    f(5);
    short s = 5;
    f(s);
}