skip to content

When you write `::println`, the function is overloaded many times. How does Kotlin decide which overload the reference points to, and what happens if it's ambiguous?

level: seniorimportance: should knowfreq 30%

answer

  1. Expected type picks the overload
  2. No expected type + multiple fits = ambiguity error
  3. Fix: annotate type or use in typed context
  4. Resolution is compile-time, fixed in bytecode
  5. Same rule for members & constructors

basics

~20 s

Kotlin uses the surrounding context — the expected function type — to pick the matching overload. If nothing constrains the type and several overloads fit, it's ambiguous and the compiler reports an error until you add a type.

solid answer

~40 s

A reference to an overloaded function is resolved by the **expected type** at the use site. If you assign `::println` to a `(Int) -> Unit`, the compiler selects the `println(Int)` overload; passing it to `forEach` on a `List<String>` selects `println(Any?)`/`println(String?)` as appropriate. When the context provides no expected functional type (e.g. `val x = ::println` with no annotation) and multiple overloads remain viable, resolution is **ambiguous** and you get a compile error like 'overload resolution ambiguity'. The fix is to supply an explicit type: `val x: (Int) -> Unit = ::println`, or in tricky cases cast/wrap in a lambda. Resolution is purely compile-time — the chosen overload is fixed in the bytecode.

code

kotlin · 8 lines
kotlin
fun main() {
    val asInt: (Int) -> Unit = ::println   // println(Int)
    asInt(42)

    listOf("x", "y").forEach(::println)     // println(Any?)

    // val ambiguous = ::println            // compile error: overload ambiguity
}

go deeper

for a junior

May only know ::println 'works in forEach' without explaining why.

for a middle

Knows the expected type matters and that a bare assignment can be ambiguous.

for a senior

Articulates expected-type-driven resolution, the ambiguity error, and the type-annotation fix; knows it's compile-time.

for a principal

Weighs API-evolution risk: adding overloads can break references or shift resolution, and designs call sites to keep the expected type explicit.

## The problem `println` has many overloads (`println()`, `println(Int)`, `println(Any?)`, `println(Char)`, ...). When you write `::println` the compiler must decide *which one* the reference denotes. ## Resolution is driven by the expected type Kotlin uses the **expected functional type** at the use site to disambiguate: ```kotlin val p: (Int) -> Unit = ::println // selects println(Int) val q: (Char) -> Unit = ::println // selects println(Char) listOf("a", "b").forEach(::println) // selects println(Any?) for String elements ``` The call site (`forEach` parameter type `(String) -> Unit`) supplies the expectation, so the overload whose signature matches is picked. ## When it's ambiguous If there's **no expected type** and several overloads remain viable, resolution fails: ```kotlin val x = ::println // ERROR: overload resolution ambiguity ``` The compiler can't choose among `println(Int)`, `println(Any?)`, etc. ### Fixes - **Annotate the type**: `val x: (Int) -> Unit = ::println`. - **Use it where the type is implied** (as a `map`/`forEach` argument). - **Wrap in a lambda** if you need a specific call shape: `{ x: Int -> println(x) }`. ## Compile-time only Unlike virtual dispatch, overload resolution for references happens entirely at **compile time**. The selected overload is baked into the generated `KFunction`/invokedynamic; there's no runtime re-selection. ## Same rules for member & constructor references Overloaded members (`Type::m`) and overloaded constructors (`::Class`) resolve identically — the expected type narrows the candidate set. This is consistent with normal Kotlin overload resolution applied to the reference's denoted callable. ## Practical guidance When exposing references in public APIs, prefer contexts where the expected type is explicit, so future overload additions don't silently change resolution or introduce ambiguity.

  • Why does `val x = ::println` fail but `list.forEach(::println)` compile?
    `forEach` supplies an expected functional type `(T) -> Unit` that selects one overload; the bare `val` with no annotation gives no expected type, leaving multiple overloads viable and thus ambiguous.
  • Is the overload chosen at runtime?
    No. Reference overload resolution is fully compile-time; the chosen target is fixed in the generated code, with no runtime re-dispatch.

saying these in an interview costs you the question

  • Claiming the JVM picks the overload at runtime
  • Saying overloaded references always pick the first/most general overload
  • Not knowing how to resolve the ambiguity (type annotation)
  • Believing ambiguity is a runtime exception rather than a compile error
  • Thinking member/constructor references use different resolution rules

context