Course contents

Method references

Method references in Java 21: static, bound, unbound, constructor and array constructor forms, signature matching, readability defaults, overload ambiguity, and eager receiver evaluation.

FreeOften askedLesson12 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: address labels for existing work
  2. Method references under the hood: the four forms
  3. When a reference is clearer, and when it is not
  4. Real world: eager bound receivers
  5. Advanced: overloads, ambiguity and constructors
  6. How to say it in the interview

🔗 What is a method reference?

A method reference is shorthand for a lambda whose whole job is to call an existing method or constructor. Order::totalCents can mean order -> order.totalCents(), and ArrayList::new can mean () -> new ArrayList<>(). It is still typed by a functional interface; the reference itself does not carry a standalone function type.

For this lesson, we'll focus on the four forms you need for interviews: static, bound instance, unbound instance, and constructor references, including array constructors like int[]::new. We'll also cover how each form maps to a target method signature, when it reads better than a lambda, and the two traps I see in code reviews: overload ambiguity and eager evaluation of a bound receiver.

Below the line (out of scope)

The 30-second answer

A method reference is a compact lambda that delegates to an existing method. The four forms are static (Integer::parseInt), bound instance (printer::print), unbound instance (String::length) and constructor (OrderDto::new, including int[]::new). The target functional interface decides the parameter and return types, so String::trim can be a Function<String,String> and String::isBlank can be a Predicate<String>. I use a method reference when the method name says the whole story; I keep a lambda when I need argument reordering, constants, conditions or a clearer parameter name. The subtle runtime rule is that obj::method evaluates obj immediately, so a null receiver throws right away, while () -> obj.method() throws only when invoked.

What you’ll learn

  • Name the four method-reference forms and translate each one to an equivalent lambda
  • Map a method reference to a functional-interface signature, including unbound receivers and constructors
  • Use constructor references and array constructors such as int[]::new correctly
  • Decide when a method reference is clearer than a lambda and when a lambda should stay
  • Handle overload and target-typing ambiguity with explicit types or casts
  • Explain why obj::method evaluates the receiver eagerly and can throw NullPointerException immediately

Interview radar

Asked often: Name the four kinds and translate each into a lambda. Core follow-up: Why is String::length unbound, and where does the receiver parameter go? Production angle: A null collaborator fails during pipeline construction because service::call evaluates the receiver immediately.

01Picture it first: address labels for existing work

A method reference is like putting an address label on work that already exists. You are not writing the work again; you are saying "when the functional interface asks, call that method". The label is only valid if the target slot has the right shape.

The four labels differ mainly in where the receiver comes from. Sometimes the receiver is already fixed (logger::info), and sometimes the receiver becomes the first parameter (String::trim).

Fig. 1

The four method-reference forms show static, bound instance, unbound instance and constructor references alongside the equivalent lambda shape and where the receiver comes from

The four method-reference forms show static, bound instance, unbound instance and constructor references alongside the equivalent lambda shape and where the receiver comes from

Keep that receiver question in your head. Most method-reference confusion is really receiver confusion.

02Method references under the hood: the four forms

Try it yourself first

Before reading on, translate each reference into a lambda and say which functional interface it fits.

Function<String, Integer> parse = Integer::parseInt;
Consumer<String> print = System.out::println;
ToIntFunction<String> length = String::length;
Supplier<ArrayList<Order>> newList = ArrayList::new;
IntFunction<int[]> newIntArray = int[]::new;

Here is the mapping I use on a whiteboard.

Kind Example Target shape Equivalent lambda
Static Integer::parseInt Function<String,Integer> s -> Integer.parseInt(s)
Bound instance System.out::println Consumer<String> s -> System.out.println(s)
Unbound instance String::length ToIntFunction<String> s -> s.length()
Constructor OrderDto::new Function<Order,OrderDto> o -> new OrderDto(o)
Array constructor int[]::new IntFunction<int[]> size -> new int[size]

A static reference is the easiest: all functional-interface arguments become method arguments. A bound instance reference stores the receiver first, then the functional-interface arguments become the method arguments. An unbound instance reference is the one to slow down for: the first functional-interface argument becomes the receiver.

Constructor references follow constructor signatures. If new OrderDto(Order) exists, then OrderDto::new fits Function<Order, OrderDto>. If new Money(long, Currency) exists, then Money::new fits BiFunction<Long, Currency, Money> with boxing on Long; for a primitive-heavy path you would usually write a domain interface like MoneyFactory { Money create(long cents, Currency currency); }.

Signature matching, not name matching

A method reference does not match by method name alone. It matches the target interface's single abstract method after overload resolution.

record Order(String id, long totalCents) {}
record OrderDto(String id, long totalCents) {
    OrderDto(Order order) {
        this(order.id(), order.totalCents());
    }
}

Function<Order, OrderDto> toDto = OrderDto::new;
Predicate<String> blank = String::isBlank;
UnaryOperator<String> trim = String::trim;
BiPredicate<String, String> startsWith = String::startsWith;

String::startsWith looks like one method, but as an unbound reference it needs two inputs for BiPredicate<String, String>: first the receiver string, then the prefix. That maps to (text, prefix) -> text.startsWith(prefix).

Notice three things:

  • The target return type matters. String::length fits ToIntFunction<String> cleanly and Function<String, Integer> with boxing. It does not fit Predicate<String>.
  • The target arity matters. System.out::println as a bound reference can fit Consumer<String>, but not BiConsumer<PrintStream, String> because the receiver is already fixed.
  • Constructors are overloaded too. ArrayList::new can fit Supplier<ArrayList<String>> for the no-arg constructor or IntFunction<ArrayList<String>> for the initial-capacity constructor.

Bound versus unbound, said slowly

The quickest way to decide between bound and unbound is to ask, "do I already have the object that will receive the call?" If yes, the receiver is bound. If no, the receiver must arrive as one of the functional-interface arguments.

PrintStream out = System.out;
Consumer<String> bound = out::println;          // text -> out.println(text)

ToLongFunction<Order> unbound = Order::totalCents; // order -> order.totalCents()
Supplier<Long> alsoBound = order::totalCents;       // () -> order.totalCents()

The method name is the same in the last two lines, but the shape is not. Order::totalCents still needs an Order, so it fits ToLongFunction<Order>. order::totalCents already has the Order, so it fits Supplier<Long> or LongSupplier depending on whether you want boxing.

This is also why ClassName::instanceMethod feels odd at first. It is not static dispatch. It is a compact way to say "take the first argument, use it as the receiver, and pass the remaining arguments to the instance method".

03When a reference is clearer, and when it is not

Try it yourself first

Which version would you keep in code review, and why?

orders.stream()
        .filter(Order::isPaid)
        .map(Order::customerEmail)
        .forEach(mailer::sendReceipt);

orders.stream()
        .filter(order -> order.totalCents() >= minimumCents)
        .map(order -> normalize(order.customerEmail()))
        .forEach(email -> mailer.sendReceipt(email));

In the first pipeline, method references read like a list of operations: is paid, get email, send receipt. The names carry the whole meaning, so the syntax gets out of the way.

In the second pipeline, I would keep the lambdas. The first captures a threshold and tells you which argument is compared. The second wraps a call. The third is equivalent to mailer::sendReceipt, but the named email parameter may be clearer if the type is not obvious nearby.

Here is my default: use a method reference when it is exactly the method you would have named anyway. Use a lambda when you transform arguments, add constants, reorder parameters, handle nulls, branch, or need a parameter name to make the code readable.

Prefer method reference Prefer lambda
Order::isPaid order -> order.isPaid() && !order.isRefunded()
String::trim s -> s.trim().toLowerCase()
BigDecimal::new text -> new BigDecimal(text, MathContext.DECIMAL64)
this::send receipt -> send(receipt, RetryPolicy.DEFAULT)
int[]::new n -> new int[Math.max(0, n)]

Many candidates repeat "prefer method references" as if it is a law. Effective Java says to prefer them when they are shorter and clearer. If the lambda explains intent better, keep the lambda.

04Real world: eager bound receivers

In the real world

I once reviewed a startup failure caused by this exact distinction. A metrics client was optional in local development, and someone passed metrics::record into a scheduler during application wiring. In environments where metrics was null, the service failed at startup before the scheduler even ran. The lambda version would still have needed a null policy, but it would have moved the failure to execution time, where the team expected it.

Bound instance references evaluate the receiver immediately. That is true even though the method call itself happens later.

class Scheduler {
    void everyMinute(Runnable task) { /* store task and run it later */ }
}

Metrics metrics = null;
Scheduler scheduler = new Scheduler();

scheduler.everyMinute(metrics::flush);      // NullPointerException now
scheduler.everyMinute(() -> metrics.flush()); // accepted now, throws when run
Fig. 2

A bound reference metrics::flush checks the metrics receiver during task creation and throws immediately when null, while a lambda stores code and reads metrics later when run

A bound reference metrics::flush checks the metrics receiver during task creation and throws immediately when null, while a lambda stores code and reads metrics later when run

This also affects mutable static fields. System.out::println captures the current PrintStream. x -> System.out.println(x) reads System.out each time it runs. Most code should not depend on that, but it is a great interview detail because it proves you understand bound references.

05Advanced: overloads, ambiguity and constructors

Overloads are where method references stop feeling magical. Because the reference has no type until the target is known, two possible targets can leave the compiler unable to choose.

void handle(Function<String, Integer> parser) {}
void handle(ToIntFunction<String> parser) {}

// handle(Integer::parseInt);        // ambiguous
handle((ToIntFunction<String>) Integer::parseInt);

void log(Consumer<String> writer) {}
void log(Predicate<String> checker) {}

// log(String::isBlank);             // picks Predicate if available alone, ambiguous with another boolean target
log((Predicate<String>) String::isBlank);

The clean fixes are boring and reliable: assign the reference to a typed variable, cast it at the call site, or avoid overloading methods on functional interfaces with similar shapes. As an API designer, I avoid overload sets like handle(Function<...>) and handle(ToIntFunction<...>) unless callers truly benefit.

There is a team-level lesson here too. If a public API accepts callbacks, give each overload a different name when the callback shapes are close. handleObject and handleInt look less elegant than one overloaded handle, but they save every caller from sprinkling casts around lambdas and method references. Readability at the call site wins.

Constructor references have the same overload story:

import java.util.*;
import java.util.function.*;

Supplier<ArrayList<String>> empty = ArrayList::new;
IntFunction<ArrayList<String>> sized = ArrayList::new;
IntFunction<int[]> ints = int[]::new;

String[] names = List.of("a", "b").toArray(String[]::new);

String[]::new is the common collection-to-array pattern because Collection.toArray(IntFunction<T[]>) asks for a function from size to array. For primitive arrays, int[]::new fits IntFunction<int[]>, which is int -> int[].

Where candidates slip

Many candidates describe Type::instanceMethod as "calling a static method on the type". That is frequently wrong. String::length is unbound: the first runtime argument becomes the receiver, so the shape is String -> int. If the receiver is already present, as in someString::length, then the shape is () -> int.

Performance angle

A method reference uses the same lambda machinery as a lambda expression: an invokedynamic call site linked by LambdaMetafactory. Do not choose it for speed. Choose it for clarity. The performance choice that can matter is the target interface: String::length as ToIntFunction<String> avoids boxing the int result, while Function<String, Integer> may box unless the JIT removes it.

There is one more readability edge case: generic methods. A method reference can sometimes express a generic target that a lambda cannot, because lambda expressions cannot declare their own type parameters. You rarely need that in application code, but it is why "method references are only shorter lambdas" is not quite complete.

06How to say it in the interview

When I ask this, I'm listening for the receiver story. A good answer names all four forms, maps at least one unbound reference correctly, and mentions the eager-null behavior of bound references without being prompted.

In the interview

"A method reference is shorthand for a lambda that only calls an existing method or constructor. The four forms are static, like Integer::parseInt; bound instance, like printer::println; unbound instance, like String::length; and constructor, like ArrayList::new or int[]::new. The target functional interface supplies the type, so String::length can be ToIntFunction<String> and the first argument becomes the receiver. I use method references when the method name is clearer than spelling out the lambda, but I keep lambdas for argument changes, constants, null handling or branching. The trap is obj::method: obj is evaluated when the reference is created, so a null receiver throws immediately."

On the whiteboard

method reference = lambda that delegates
target functional interface still supplies the type

static:       Integer::parseInt  == s -> Integer.parseInt(s)
bound:       out::println       == x -> out.println(x)
unbound:     String::length     == s -> s.length()
constructor: ArrayList::new     == () -> new ArrayList<>()
array ctor:  int[]::new         == n -> new int[n]

unbound receiver = first parameter
bound receiver evaluated immediately
overload ambiguity: type variable | cast | avoid overloads
clarity rule: reference when name says it all, lambda when logic changes
Practise this topicLambdas & functional interfaces: interview questions & scenarios
Further reading (6)