When both a bound and an unbound interpretation of a `::method` reference could satisfy the expected type, how does Kotlin resolve it, and where can ambiguity or surprises arise?
answer
- Form picks bound/unbound; expected type picks the overload
- Unbound applies the +1 receiver rule before matching
- Companion collision: Foo::bar vs Foo.Companion::bar
- Ambiguous overloads -> cast, annotate, or lambda
- Member vs extension name clashes need qualification
basics
~20 sKotlin uses the expected function type to pick the right meaning. Whether you wrote a type name or an instance, plus how many parameters the target slot has, decides bound vs unbound; sometimes you must spell it out to remove ambiguity.
solid answer
~50 sThe form of the left-hand side already separates the two cases: `Type::method` is unbound, `instance::method` is bound — these are syntactically distinct. The interesting resolution happens with the *expected type*: when a `::method` reference appears in a slot expecting a specific function type, the compiler matches arity and parameter types against the candidate member set, choosing the overload whose unbound or bound shape fits the target. Surprises arise when: (1) a class has multiple overloads of `method` and several fit the expected type, forcing an explicit cast or lambda; (2) a name resolves to both a member and an extension; (3) a companion `object`'s name collides with the class name so `Foo::bar` could mean the class's unbound member or the companion's member; and (4) SAM conversions for Java interfaces interact with the receiver arity. The fix is usually to annotate the expected type, wrap in a lambda, or use a fully qualified reference.
code
kotlin · 5 linesclass P { fun f(x: Int) = x; fun f(x: String) = x.length }
val g: (P, Int) -> Int = P::f // Int overload chosen by expected type
val h: (P, String) -> Int = P::f // String overload chosen
println(g(P(), 3)) // 3
println(h(P(), "hi")) // 2go deeper
Understands that Type::m vs instance::m is chosen by what you write on the left.
Knows the expected function type guides which overload a reference resolves to.
Resolves overload ambiguity with annotations/casts/lambdas and recognizes member-vs-extension clashes.
Reasons about companion collisions, SAM interop, and designs overload sets and APIs that stay reference-friendly under the +1 receiver rule.
## The two forms are syntactically distinct `instance::method` (bound) and `Type::method` (unbound) differ by what is on the left. So there is no ambiguity about bound-vs-unbound *per se* — you choose by writing a value or a type. Ambiguity instead comes from **which member** the name resolves to and **which function type** is expected. ## Expected-type-driven resolution A callable reference is resolved against the **expected type** of the context. The compiler enumerates candidate callables for the name, computes each candidate's reference type (applying the +1 receiver rule for unbound), and keeps those assignable to the expected type. ```kotlin class P { fun f(x: Int) = x; fun f(x: String) = x.length } val g: (P, Int) -> Int = P::f // picks the Int overload by expected type val h: (P, String) -> Int = P::f // picks the String overload ``` Here the expected type disambiguates overloaded `f` for the unbound reference. ## Ambiguity cases ### 1. Multiple fitting overloads If two overloads both match the expected type, you get an ambiguity error and must cast or use a lambda: ```kotlin // val r = P::f // error if expected type is too loose val r = P::f as (P, Int) -> Int // explicit ``` ### 2. Member vs extension A name available as both a member and an extension on the same type can be ambiguous; qualify or use a lambda `{ it.method() }`. ### 3. Companion-object name collision When a class has a companion, `Foo::bar` may resolve to an unbound member of `Foo` or a member of the companion object (which is itself a bound-like reference to the singleton). Use `Foo.Companion::bar` to be explicit. ```kotlin class Foo { fun bar() = 1; companion object { fun bar() = 2 } } val a = Foo::bar // unbound member: (Foo) -> Int val b = Foo.Companion::bar // bound to the companion: () -> Int ``` ### 4. SAM / Java interop When the target is a Java functional interface, SAM conversion plus the receiver-arity rule can change which form fits; being explicit with the expected type avoids surprises. ## Practical guidance - Prefer assigning the reference to a variable with an explicit function type, or pass it into a slot with a known type, so resolution is deterministic. - For overload ambiguity, the cleanest fixes are an explicit type annotation, an `as` cast, or a lambda that names the call. - Remember the unbound arity (+1 receiver) when matching against the expected type — it is the most common source of a confusing mismatch. ## Why this is a principal-level concern API authors deciding whether to expose methods that are commonly passed as references should keep overload sets reference-friendly (avoid overloads that collide under the +1 rule) and document whether bound or unbound usage is intended, because the ergonomics of `map(Type::m)` vs `op(instance::m)` depend on it.
- How do you disambiguate `Foo::bar` when `Foo` has both a member `bar` and a companion `bar`?Use `Foo::bar` for the unbound member `(Foo) -> ...` and `Foo.Companion::bar` for the companion's `bar` bound to the singleton.
- Two overloads both match the expected type — what are your options?Add an explicit function-type annotation, apply an `as` cast to the desired type, or replace the reference with a lambda that names the specific call.
saying these in an interview costs you the question
- Claiming bound vs unbound is itself ambiguous regardless of syntax
- Ignoring the +1 receiver rule during overload matching
- Not knowing companion-object name collisions exist
- Saying Kotlin always picks the first overload
- Believing a lambda cannot resolve a reference ambiguity