skip to content

Explain the difference between a bound and an unbound instance-method reference, including how the receiver is determined in each.

level: middleimportance: must knowfreq 68%

answer

  1. Object before :: = bound, captured receiver
  2. Type before :: = unbound, first param IS the receiver
  3. String::compareTo = (a,b) -> a.compareTo(b)
  4. Comparator.comparing(Person::getAge) is unbound
  5. Static-vs-instance overlap can make it ambiguous

basics

~20 s

In a bound reference (object::method) the object the method runs on is fixed when you write it. In an unbound reference (Class::method) that object is not fixed — it's passed in as the first argument each time the function is called.

solid answer

~50 s

Both forms look like X::method, but X differs. In a bound reference, X is an actual object expression (e.g. user::getName, System.out::println). The receiver is captured once at the reference site, so the abstract method's parameters map to the method's own parameters. In an unbound reference, X is a type name (e.g. String::length, String::toUpperCase). No receiver is captured; instead the target interface's first parameter becomes the receiver, and any remaining parameters map to the method's arguments. So String::toUpperCase fits Function<String,String> (the String arg is the receiver), and String::compareTo fits Comparator<String> ((a,b) -> a.compareTo(b)). The compiler disambiguates by checking which reading type-checks against the target functional interface; if a type has both a matching static and a matching instance method, it can be ambiguous and fail to compile. Bound references also capture and retain their receiver object.

go deeper

for a junior

Knows object::method binds a fixed object and Class::method does not, with one example of each.

for a middle

Can explain the first-parameter-becomes-receiver rule and map String::compareTo to a Comparator.

for a senior

Can reason about capture/evaluation timing of the bound receiver and recognize static-vs-instance ambiguity that forces a lambda.

for a principal

Can predict resolution outcomes against arbitrary target types, advise on API design that avoids ambiguous references, and discuss capture/memory implications in hot paths.

## Setup: receiver, lambda, functional interface The **receiver** of an instance method is the object the method is invoked on — the `this` inside the method. In `user.getName()`, `user` is the receiver. A **functional interface** has one abstract method; a **method reference** (`::`) provides its implementation by pointing at an existing method. To know whether a reference is legal, the compiler matches the reference's effective parameter list against the abstract method's signature (the **target type**). ## Bound instance-method reference: `instance::method` The part before `::` is an **object expression** that is evaluated, and its value is the **fixed receiver**. The reference is equivalent to `(a, b, ...) -> instance.method(a, b, ...)` where `a, b, ...` are the abstract method's parameters and `instance` is captured once. - `System.out::println` ⟶ `x -> System.out.println(x)` — `System.out` is the receiver, `x` is the method argument. Fits `Consumer<T>`. - `user::getName` ⟶ `() -> user.getName()` — fits `Supplier<String>`. Because the receiver is captured, the resulting functional object **holds a reference to that object** (relevant for closures, memory, and evaluation timing: the expression before `::` is evaluated when the reference is created, not at each call). ## Unbound instance-method reference: `ClassName::method` The part before `::` is a **type name**. No receiver is captured. Instead, **the first parameter of the abstract method becomes the receiver**, and the remaining parameters become the method's arguments. The reference is equivalent to `(receiver, b, c, ...) -> receiver.method(b, c, ...)`. - `String::length` ⟶ `s -> s.length()` — fits `ToIntFunction<String>` / `Function<String,Integer>`. The single param is the receiver; the method takes no further args. - `String::toUpperCase` ⟶ `s -> s.toUpperCase()` — fits `Function<String,String>`. - `String::compareTo` ⟶ `(a, b) -> a.compareTo(b)` — fits `Comparator<String>`. The first param `a` is the receiver, the second `b` is `compareTo`'s argument. This is the classic example showing an unbound reference with an extra argument. ## How the compiler tells them apart The syntax `X::method` is ambiguous on its face — `X` could be a variable holding an object (bound) or a class name (unbound). Resolution rules: 1. If `X` is an **expression/object**, it is a bound reference. 2. If `X` is a **type**, the compiler tries the unbound reading (first param = receiver). It may *also* try a static-method reading if a static method of that name exists. If a type has both a static method and an instance method that would both type-check against the target interface, the reference is **ambiguous** and is a compile error — you must fall back to an explicit lambda. Example pitfall: a class with both `static parse(String)` and an instance `parse()` could make `MyType::parse` ambiguous depending on the target. ## Why it matters in practice - `list.forEach(System.out::println)` (bound) vs `stream.map(String::toUpperCase)` (unbound) — both read cleanly but rely on opposite receiver mechanics. - Comparators: `Comparator.comparing(Person::getAge)` uses an *unbound* reference (`Person::getAge` = `p -> p.getAge()`), which is why it works as a key extractor over arbitrary `Person` objects. - Bound references capture state; in a hot loop, repeatedly recreating a bound reference re-evaluates the receiver expression, whereas a static/unbound reference captures nothing.

  • Why does Comparator.comparing(Person::getAge) compile even though comparing expects a Function<Person,U>?
    Person::getAge is an unbound reference equal to p -> p.getAge(), which is exactly a Function<Person,Integer>: the Person is the receiver/first param, and getAge() returns the key.
  • When is X::method ambiguous?
    When the type X has both a static method and an instance method of that name that each type-check against the target functional interface; the compiler cannot choose and you must use an explicit lambda.

saying these in an interview costs you the question

  • Thinking the first parameter of an unbound reference is a normal argument rather than the receiver.
  • Believing bound references re-evaluate their receiver on every call (the receiver expression is evaluated once, when the reference is created).
  • Assuming Class::method is always a static call.

context