skip to content

Describe the three phases of Java's overload-resolution algorithm. Why are they ordered the way they are?

level: seniorimportance: should knowfreq 55%

answer

  1. Three phases: widen -> box -> vararg
  2. Phase 1: no boxing, no varargs
  3. Phase 2: boxing/unboxing allowed
  4. Phase 3: varargs considered
  5. Order protects pre-Java-5 backward compatibility

basics

~20 s

Java tries to find the right overload in three passes: first without auto-converting primitives (no boxing) and without varargs, then allowing boxing/unboxing, and finally allowing varargs. It uses the first phase that finds a match.

solid answer

~50 s

When several overloads share a name, the compiler runs three sequential phases and stops at the first that yields exactly one most-specific applicable method. Phase 1: applicability by strict invocation — only widening primitive conversions and reference widening (subtyping) are allowed; no boxing/unboxing, no varargs. Phase 2: applicability by loose invocation — same as phase 1 but now autoboxing and unboxing are also permitted; still no varargs. Phase 3: applicability by variable-arity invocation — varargs methods are considered, with boxing allowed. The ordering encodes a backward-compatibility and least-surprise principle: features added later to the language (autoboxing in Java 5, varargs in Java 5) must never "steal" a call that an older, cheaper, exact-style match could satisfy. So an exact/widening match always beats a boxing match, which always beats a varargs match. If a phase finds more than one applicable method it then picks the most specific; if it cannot disambiguate, you get an ambiguity compile error.

code

java · 7 lines
java
static void f(int x)      { System.out.println("int (phase 1)"); }
static void f(Integer x)  { System.out.println("Integer (phase 2)"); }
static void f(int... xs)  { System.out.println("varargs (phase 3)"); }

f(5);        // -> int (phase 1): exact/widening match wins
// If f(int) is removed, f(5) -> Integer (phase 2): boxing
// If both removed, f(5) -> varargs (phase 3)

go deeper

for a junior

May only know overloads are chosen by argument types; not expected to recite the three phases.

for a middle

Can name the three phases (widen, box, vararg) and give a simple example of each.

for a senior

Explains each phase's allowed conversions precisely and articulates the backward-compatibility rationale for the ordering.

for a principal

Connects the phase ordering to language-evolution and source-compatibility guarantees, and can reason about subtle real-world resolution bugs (primitive vs wrapper overloads, vararg shadowing) when designing APIs.

### Setup: the problem At a call site like `f(x, y)` the compiler has a *set* of methods named `f` that are visible. It must choose exactly one. To do this without surprising results, the Java Language Specification (JLS §15.12.2) defines **three ordered phases**. The compiler tries phase 1; if that finds a single best method it stops. Otherwise it tries phase 2, then phase 3. This is sometimes summarized as **"widen, box, vararg"**. ### The conversions, defined To follow the phases you need a few terms: - **Widening primitive conversion**: a smaller primitive auto-converts to a larger one without loss, e.g. `int -> long -> float -> double`, `byte -> short -> int`. This is cheap and always safe. - **Reference widening (subtyping)**: a subtype reference is usable where a supertype is expected, e.g. `String -> Object`. - **Boxing / unboxing**: converting between a primitive and its wrapper, e.g. `int <-> Integer`, `double <-> Double`. Added in Java 5. - **Varargs (variable arity)**: a trailing `Type...` parameter that accepts zero or more arguments, internally an array. Added in Java 5. ### Phase 1 — strict invocation (no boxing, no varargs) A method is **applicable by strict invocation** if each argument is assignable to the corresponding parameter using only **widening primitive** and **reference widening** conversions (plus identity). Boxing/unboxing and varargs are forbidden. Example: for `f(int)` and `f(long)` called with a `short`, both are applicable (short widens to int and to long); the compiler then picks the **most specific** — `f(int)` wins because `int` is more specific than `long` (int is assignable to long). ### Phase 2 — loose invocation (boxing allowed, still no varargs) If no method matched in phase 1, the compiler retries allowing **autoboxing/unboxing** in addition to widening. Example: `f(Integer)` called with an `int` succeeds only here, because it needs boxing. This is why an exact primitive overload is preferred over a wrapper overload: phase 1 caught the primitive one first. ### Phase 3 — variable-arity invocation (varargs considered) Only if phases 1 and 2 both fail does the compiler consider **varargs** methods, now also permitting boxing. Example: `f(int...)` is chosen for `f(1, 2, 3)` only when no fixed-arity method fits. This is why `f(int a, int b)` beats `f(int... xs)` for a two-argument call. ### Why this exact order? The ordering is a deliberate **backward-compatibility guarantee**. Autoboxing and varargs were both introduced in Java 5. Code written against Java 1.4 chose overloads using only widening. If boxing or varargs were tried *first* (or equally), recompiling old code on Java 5 could silently switch which method is called — a serious source-compatibility break and a performance footgun (boxing allocates). By making newer, more permissive features the **last resort**, the JLS ensures: (1) old code keeps resolving the same way, and (2) the cheaper conversion always wins, since widening < boxing < array-allocation in cost. ### Within a phase: most-specific A phase can find **several** applicable methods. It then keeps the **most specific** one: method A is more specific than B if any argument list that A accepts, B would also accept (A's parameter types are assignable to B's). If exactly one most-specific method exists, that's the winner. If two are mutually-applicable and neither is more specific, the call is **ambiguous** — a compile error. ### Worked example ``` void g(Object o) {} // (a) void g(Integer i) {} // (b) void g(int x) {} // (c) g(5); ``` Phase 1: `5` is an `int`. (c) `g(int)` matches by identity. (a)/(b) need boxing so they're *not* applicable in phase 1. Phase 1 finds exactly one -> **(c) wins**, no boxing, no further phases. ### Practical takeaways - Prefer designing overloads so the **intended** one matches in phase 1. - Beware mixing primitive and wrapper overloads — the phase order, not your intuition, decides. - A redundant varargs overload alongside fixed-arity ones is harmless because varargs is last.

  • Why isn't autoboxing tried in the first phase?
    To preserve backward compatibility: autoboxing arrived in Java 5, and trying it first could change how pre-Java-5 code resolves overloads on recompile. It also avoids needless boxing allocations when a cheaper widening match exists.
  • Given f(long) and f(Integer), which is called for f(intValue)?
    f(long). Phase 1 allows widening (int->long) and finds f(long); f(Integer) needs boxing, which is only allowed in phase 2, so phase 1 already settles it.

Like hiring: first interview only perfect-fit candidates (exact/widening). If none, broaden to those who'd need light retraining (boxing). Only if still empty, consider a temp agency that sends a whole team (varargs). You never reach for the costlier option while a cheaper exact fit exists.

saying these in an interview costs you the question

  • Saying boxing and widening are tried together
  • Claiming varargs is considered first or equally
  • Thinking the phases pick by runtime values
  • Believing the JVM (not javac) performs the phase selection

context