A method reference is compiler shorthand for a lambda that does nothing but call one existing method. String::toUpperCase and s -> s.toUpperCase() compile to the same invokedynamic call site. The value of method references is not brevity for its own sake — it is that they force the parameter mapping to be exact, so the compiler rejects a whole class of argument-order mistakes that a lambda would happily accept.

The four kinds

Every method reference is one of four shapes. The distinction matters because it changes what becomes the receiver and what becomes an argument:

KindSyntaxEquivalent lambda
StaticInteger::parseInts -> Integer.parseInt(s)
Bound instancelogger::infomsg -> logger.info(msg)
Unbound instanceString::toUpperCases -> s.toUpperCase()
ConstructorArrayList::new() -> new ArrayList<>()

The subtle pair is bound vs unbound. In a bound reference the receiver is fixed at the point you write the reference (logger is captured now). In an unbound reference the receiver is supplied later as the first functional-interface argument — which is why String::toUpperCase satisfies Function<String,String>: the single input string becomes the receiver of toUpperCase().

Advertisement

How the compiler resolves one

A method reference is resolved against the target type — the functional interface the expression is being assigned to. The compiler takes that interface's single abstract method's signature and looks for a method that can be invoked with those parameters, threading the receiver in for the unbound case.

// target type Function<String,Integer>: (String) -> Integer
Function<String,Integer> f = Integer::parseInt;   // matches static int parseInt(String)

// target type BiFunction<String,String,Boolean>: (String,String) -> Boolean
BiFunction<String,String,Boolean> eq = String::equals; // unbound: 1st arg is receiver

Because resolution depends on the target type, the same reference text can mean different things in different contexts — and an unresolvable one is a compile error, never a runtime surprise.

When you cannot use one

A method reference only works when the call is a pure pass-through. The moment you need to touch the arguments, add a constant, reorder, or call more than one method, you must drop back to a lambda:

list.stream().map(String::trim)          // OK: pure pass-through
list.stream().map(s -> s.trim().toLowerCase())  // needs a lambda: two calls
list.stream().map(s -> "id-" + s)              // needs a lambda: transforms the arg
list.stream().filter(s -> s.length() > 3)      // needs a lambda: uses a literal

This is the actual decision rule — not style preference. If the body is exactly one existing method applied to the parameters in order, a method reference is available; otherwise it is not.

Advertisement

Gotchas that bite in production

The bound receiver is evaluated eagerly. obj::method captures obj at the point the reference is created, exactly like a lambda closing over it. If obj is null there, you get an immediate NullPointerException — not a deferred one when the function is finally called.

Runnable r = maybeNull::run;   // NPE thrown HERE if maybeNull is null,
                               // even though run() is never invoked

Overload ambiguity. If the target type could match more than one overload of the referenced method, the reference is rejected as ambiguous and you must disambiguate with an explicit lambda. Side effects in the receiver expression run once, at capture time — a common trap with getService()::call when getService() is not idempotent.

Reach for a method reference only when the lambda body is a single existing method applied to the parameters unchanged and in order. Its real payoff is that the compiler verifies the parameter mapping exactly — but remember the bound receiver (obj::m) is captured eagerly, so a null there fails at creation, not at call.