skip to content

A lambda is a poly expression. What concrete problems does that cause with method overloading, and how do you resolve a resulting ambiguity?

level: seniorimportance: should knowfreq 38%

answer

  1. poly expression ⇒ no fixed type ⇒ overload resolution can't choose
  2. compiler prunes by arity + void/value compatibility
  3. two applicable functional-interface overloads ⇒ ambiguous
  4. fix: cast `(Runnable) () -> ...` or typed variable
  5. API lesson: don't overload on different functional interfaces

basics

~20 s

Because a lambda has no fixed type, when a method is overloaded with two functional-interface parameters that both fit the lambda, the compiler can't choose and reports an ambiguity. You fix it by giving the lambda a concrete type — usually with an explicit cast like (Runnable) () -> ....

solid answer

~50 s

A lambda is a **poly expression** — it has no type until a target type is supplied. With overloaded methods this creates trouble: if `m(Runnable)` and `m(Callable<String>)` both exist, the lambda `() -> "x"` is compatible with both (a `Runnable` ignoring the return value, or a `Callable` returning the string), so the call is **ambiguous** and won't compile. The compiler uses the lambda's *shape* — arity and whether the body produces a value — to narrow candidates, but when two functional-interface overloads remain applicable it cannot pick a most-specific one. **Resolutions:** (1) **cast** the lambda to the intended interface — `m((Callable<String>) () -> "x")`; (2) assign it to a typed variable first and pass that; (3) use a **method reference** if it resolves unambiguously; or (4) avoid overloading methods on different functional interfaces in your own API design. The cast is the standard, localized fix.

code

java · 8 lines
java
import java.util.concurrent.Callable;

static void run(Runnable r)        { /* ... */ }
static <T> void run(Callable<T> c) { /* ... */ }

// run(() -> "hello");                  // ERROR: ambiguous (value-compatible fits both)
run((Callable<String>) () -> "hello");  // OK: cast pins the target type
run(() -> System.out.println("hi"));     // OK: void-compatible -> only Runnable applies

go deeper

for a junior

Awareness that overloaded methods plus lambdas can produce a compile error is enough.

for a middle

Can recognize the ambiguity and fix it with an explicit cast or a typed variable.

for a senior

Explains void- vs value-compatibility, how shape prunes candidates, and the API-design rule against overloading on functional interfaces.

for a principal

Designs APIs to avoid these traps (distinct method names, not overloads), and can reason about the interaction with generic method type inference and target typing across the call site.

## Recap: poly expression A **poly expression** is one whose type depends on context. A lambda is the canonical example — `() -> "x"` has no inherent type; the compiler assigns it one from the **target type**. Overload resolution is where this contextual nature bites, because overload resolution is itself about *choosing* the target. ## Why overloading clashes with lambdas Normally the compiler picks an overload by finding the **most specific** applicable parameter type for the argument. With a lambda argument there is no argument type to compare — only a lambda *shape*. The compiler does use the shape to prune: - **Arity** — the lambda's parameter count must match the interface's abstract method. - **Void-compatible vs value-compatible body** — an expression-statement body (e.g. `() -> doSomething()`) is *void-compatible*; an expression that yields a value (e.g. `() -> "x"`) is *value-compatible*. A block body is judged by whether all its `return`s carry a value. If, after pruning, **two or more functional-interface overloads are still applicable**, and neither's interface is more specific than the other, the call is **ambiguous** and fails to compile. ### Concrete example ```java void run(Runnable r) { /* ... */ } <T> void run(Callable<T> c) { /* ... */ } run(() -> "hello"); // AMBIGUOUS ``` `() -> "hello"` is value-compatible. It fits `Callable<String>` (returns the string) and *also* fits `Runnable` — a `Runnable` body may be an expression whose value is simply discarded. Both overloads apply; neither `Runnable` nor `Callable` is a subtype of the other, so there is no most-specific choice. ## How to resolve it 1. **Explicit cast (standard fix):** state the target type inline. ```java run((Callable<String>) () -> "hello"); ``` The cast *is* a target-typing context, so the lambda's type is pinned and only the matching overload applies. 2. **Typed intermediate variable:** ```java Callable<String> c = () -> "hello"; run(c); ``` Now the argument has a concrete type; ordinary overload resolution works. 3. **Method reference**, when it resolves unambiguously to one interface. 4. **API design:** avoid declaring overloads that differ only in functional-interface parameter type. This is a known JDK lesson — for example, designers often give the methods *different names* (`submit` vs `execute`) instead of overloading. ## Void- vs value-compatibility nuance A subtle case: `() -> System.out.println("x")` is an expression statement (the call returns `void`), so it is **void-compatible only**, and it would match `Runnable` but **not** `Callable<T>`. Changing the body from a value expression to a void expression can therefore *change* which overloads apply — a frequent source of confusion when refactoring. ## Why the language is built this way Lambdas were retrofitted onto a nominal type system that already had overloading. Rather than inventing structural function types, Java made lambdas poly expressions resolved by target typing. The cost is exactly these overload ambiguities; the benefit is that lambdas integrate seamlessly with the vast existing library of single-method interfaces.

  • Why is `run(() -> "x")` ambiguous when overloads `run(Runnable)` and `run(Callable<String>)` exist?
    `() -> "x"` is value-compatible: it fits `Callable` (returns the string) and also `Runnable` (a value expression whose result is discarded). Both overloads apply and neither interface is more specific, so the compiler cannot choose — a compile error. Casting the lambda to the intended interface fixes it.
  • Does `() -> System.out.println("x")` have the same ambiguity?
    No. That body is an expression *statement* returning void, so it is only void-compatible. It matches `Runnable` but not `Callable<T>`, so the overload resolves unambiguously to `run(Runnable)`.

saying these in an interview costs you the question

  • Claiming overload resolution sees a lambda's 'class' to choose the overload
  • Thinking the ambiguity is a runtime error rather than a compile error
  • Believing a return-value lambda can never match a `Runnable` (it can — the value is discarded)
  • Resolving ambiguity by changing the lambda body's logic instead of casting

context