AlgoMaster Logo

Method References

High Priority24 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

A method reference is a shorthand for a lambda that does nothing but call an existing method. Any lambda of the form x -> someMethod(x) can be expressed as Type::someMethod or instance::someMethod. This lesson covers the four kinds, how the compiler decides which one a piece of syntax means, and the few situations where the shorthand can't replace the lambda.

What a Method Reference Is

A lambda is a small block of code that fits a functional interface. Most lambdas do real work in their body: arithmetic, conditionals, building a string. A surprising fraction, though, do nothing except call one existing method.

The lambda price -> System.out.println(price) takes one argument and hands it straight to System.out.println. There's no transformation, no filtering, no extra logic. The lambda is a thin wrapper around a method call.

For that shape, Java offers a shorter syntax:

System.out::println reads as "the println method on the object System.out". The compiler turns it into a Consumer<String> whose accept method calls System.out.println(arg). Same behaviour, less code, and arguably a clearer expression of intent: "for each price, print it" instead of "for each price, take it as price, then print price".

The general form is Target::methodName. The :: operator (two colons) separates the target from the method name. The target can be a class, an instance, a this expression, or super. The method name is just the bare identifier; no parentheses, no argument list. The compiler figures out the rest by matching the reference against the functional interface it's being assigned to.

There are four kinds of method references, and that's the heart of this lesson. Each one resolves to a slightly different lambda:

  1. Static method reference: ClassName::staticMethod
  2. Bound instance method reference: instance::instanceMethod
  3. Unbound instance method reference: ClassName::instanceMethod
  4. Constructor reference: ClassName::new

The next four sections take them one at a time.

Static Method References

A static method reference points to a static method on some class. The reference is ClassName::staticMethodName. The arguments to the functional interface become the arguments to the static method, in order.

Integer::parseInt references the static method Integer.parseInt(String). In context, map needs a Function<String, Integer>. The compiler matches the function's single String parameter to parseInt's String parameter, and the function's Integer return type to parseInt's int return (auto-boxed). The reference is exactly equivalent to s -> Integer.parseInt(s).

Another common case is referencing a static method on the same class. Suppose each product can be compared by price, and the comparator lives as a static method:

sort wants a Comparator<Product>, whose compare(a, b) method takes two Products and returns an int. ProductSorter::compareByPrice has exactly that shape: two Product arguments, an int return. The compiler wires them together. The equivalent lambda is (a, b) -> ProductSorter.compareByPrice(a, b).

The pattern works for any static method whose parameter list and return type fit the target functional interface. Math::max is a BinaryOperator<Integer> (or IntBinaryOperator, depending on context). String::valueOf can be a Function<Object, String>. The compiler does the matching at compile time, so a mismatched signature is a compile error, not a runtime surprise.

Bound Instance Method References

A bound instance method reference points to a method on a specific object. The reference is instance::instanceMethod. The object is captured (bound) at the moment the reference is created; calling the resulting functional interface invokes the method on that captured object.

customer::greet captures the specific customer instance. The resulting Function<String, String> takes one String argument (the greeting) and invokes customer.greet(greeting). The equivalent lambda is greeting -> customer.greet(greeting). The receiver is fixed; the argument is what varies across calls.

The "bound at the time the reference is taken" part matters. If the variable is reassigned after the reference is created, the reference still points to the original object:

The first println prints "Customer: Priya" because describer captured the original customer object back when the reference was created. The reassignment changed what the local variable customer points to, but didn't change what the reference holds. This is the same capture rule that lambdas use for effectively final variables, and it has the same practical effect.

The most familiar bound reference is System.out::println. System.out is an instance of PrintStream (a specific one, the standard output stream). Writing System.out::println captures that stream and references its println method. The reference accepts whatever println accepts (often Object, sometimes a primitive overload).

A common production pattern is logging:

Output (varies by logger config):

logger::info binds the logger instance. Each call inside forEach invokes logger.info(orderId). The receiver is fixed; the argument varies. That's the defining property of a bound reference.

Unbound Instance Method References

An unbound instance method reference points to an instance method, but the receiver isn't fixed. The reference is ClassName::instanceMethodName. The first argument to the functional interface becomes the receiver of the method call; any remaining arguments become the method's arguments.

String::toLowerCase reads as "the toLowerCase method on String, applied to whatever string you give it". The target type is Function<String, String>. The function's single String argument becomes the receiver of toLowerCase. The equivalent lambda is s -> s.toLowerCase().

The contrast with a bound reference is the source of most of the confusion around method references:

  • someString::toLowerCase is bound: a specific string is captured, calling the resulting Supplier<String> returns the lowercase form of that specific string.
  • String::toLowerCase is unbound: no string is captured, calling the resulting Function<String, String> requires a string argument that becomes the receiver.

Both are valid syntax, both compile, both call the same method. They differ in arity (the bound form takes zero extra arguments, the unbound form takes one) and in which String plays the role of the receiver.

When an unbound reference points to an instance method that already takes arguments, those arguments line up after the receiver.

Here the lambda inside filter is c -> c.startsWith("WELCOME"). The body calls an instance method with one argument. The unbound reference form is harder to express cleanly because the prefix is constant rather than a parameter; in this case the lambda is more readable than any reference form, so it stays as a lambda.

The shape that does work as an unbound reference is when both pieces vary.

Product::name is an unbound instance method reference. Comparator.comparing expects a Function<Product, ?>, so the function's single argument becomes the receiver of name(), and the return is the String name. The equivalent lambda is p -> p.name(). The compiler doesn't fix a particular Product; each call passes a different one.

What's wrong with this code?

The compiler reports an arity mismatch. anchor::name is a bound reference: it captures anchor and accepts zero arguments, so it can only match a Supplier<String>, not a Function<Product, String>. The assignment to Function<Product, String> requires a function that accepts a Product argument, which the bound form doesn't.

Fix: Use the unbound form (the class name as the target) so the first argument becomes the receiver.

Now the reference is unbound: each call to nameOf.apply(p) invokes p.name(), with p as the receiver. The mental model is: instance::method captures the instance, Class::method (when method is an instance method) treats the first argument as the instance.

How the Compiler Picks Between Bound and Unbound

When ClassName::methodName is written and the method exists both as a static method and as an instance method on the class, or when both forms would satisfy the target type, the compiler has to choose. It does so by matching the reference's potential shapes against the target functional interface's signature.

The target type is the functional interface that the reference is being assigned to or passed as. The compiler reads the reference, lists the possible interpretations, and keeps the one whose arity and parameter types match the target. If more than one interpretation matches, the call is ambiguous and the compiler reports an error.

For String::length, there's only one possible interpretation: length is an instance method, so the reference is unbound, and the first argument is a String whose length() is called. It matches Function<String, Integer> (since int auto-boxes) or ToIntFunction<String>.

For Integer::parseInt, there's one possible interpretation: parseInt(String) is static, so the reference takes a String and returns an int. It matches Function<String, Integer> or ToIntFunction<String>.

The two shapes share a target type (Function<String, Integer>) but the compiler can still tell them apart because their internal structure is different: one is "call a static method", the other is "call an instance method on the first argument".

The interesting case is when the target type's arity forces a choice:

coupon::toUpperCase and String::toUpperCase look almost the same, but the receiver differs. The first form binds the specific coupon string; the second leaves the receiver open. The compiler uses the target type's arity (Supplier takes zero, Function takes one) to decide which form fits.

Constructor References

A constructor reference points to a constructor. The reference is ClassName::new. The arguments to the functional interface become the arguments to the constructor; the return is a new instance.

Product::new references the constructor Product(String). The target type is Function<String, Product>, whose single String parameter matches the constructor's parameter. Calling apply("Notebook") runs new Product("Notebook"). The equivalent lambda is name -> new Product(name).

A common use is creating empty collections or empty containers. The target type's parameter list picks the constructor.

Each Supplier calls the no-arg constructor of its target type when get() runs. ArrayList::new resolves to the no-arg ArrayList() because the target is Supplier<List<String>> (zero arguments, returns a list). If the target were Function<Integer, List<String>>, the same ArrayList::new reference would resolve to ArrayList(int initialCapacity). The compiler uses the target type's parameter list to pick the right overloaded constructor.

makeCart.apply(100) runs new ArrayList<>(100), which constructs an ArrayList with an initial capacity of 100. The capacity is internal; the externally visible size is still 0 right after construction, and 1 after one add. Different target type, different constructor, same ArrayList::new reference.

For records and custom classes, constructor references give a clean way to lift a constructor into a function.

Here the lambda is r -> new Customer(r[0], r[1]). There's no constructor reference shorter than this lambda, because the function isn't a straight call to a two-argument constructor: it has to pull out r[0] and r[1] first. If the upstream produced pairs directly, a BiFunction<String, String, Customer> could use Customer::new cleanly. The reference form works only when the call shape exactly matches the constructor's argument list.

Product::new builds a fresh object on every call. In a hot loop or a long stream, that allocation cost adds up. The same is true for the equivalent lambda; method references aren't faster or slower than their lambdas, they're just shorter.

The Four Kinds at a Glance

The table below collects the four kinds and shows how each one rewrites to an equivalent lambda.

KindSyntaxWhen it appliesEquivalent lambdaE-commerce example
StaticType::staticMethodMethod is static on Type(a, b) -> Type.staticMethod(a, b)Integer::parseInt for stock strings
Bound instanceinstance::instanceMethodReceiver is a specific object captured hereargs -> instance.instanceMethod(args)System.out::println for logging cart items
Unbound instanceType::instanceMethodReceiver is the first argument; Type is the receiver's class(receiver, args) -> receiver.instanceMethod(args)Product::getName for Comparator.comparing
ConstructorType::newThe reference builds a new Typeargs -> new Type(args)ArrayList::new for a fresh cart

A useful rule of thumb: if the part before :: is a value (a variable, this, super, or any expression that can be passed around), the reference is bound and the receiver is fixed. If the part before :: is a type name, the reference is either static (for static methods), unbound (for instance methods, with the first argument as receiver), or a constructor (for ::new).

The compiler resolves which interpretation applies by checking the target functional interface. The target's arity and parameter types determine which shape fits. When more than one interpretation would fit, the call is ambiguous.

Compatibility and Limitations

A method reference compiles only when its shape matches the target functional interface. "Shape" means three things: the number of parameters, the types of those parameters (after the receiver, if any), and the return type.

Match rules:

  • The number of arguments the reference takes must equal the number of arguments the functional interface's abstract method takes (for unbound references, the receiver counts as the first argument).
  • The argument types must be assignable from the target's parameter types.
  • The return type must be assignable to the target's return type, or void if the target returns void.

That last point allows a method that returns a value to be referenced against a Consumer. Consumer<T> returns void, so any reference that takes a T and produces something is acceptable; the produced value is just discarded.

Cannot reference overloaded methods unless the target type disambiguates. When a class has two methods with the same name but different signatures, the reference is ambiguous on its own. The target type has to pick one.

The two references have the same text (System.out::println) but resolve to different overloaded println methods because their target types differ. Assigning the reference to a generic variable with no clear target type would cause the compiler to report an ambiguity.

Cannot pass extra arguments. A method reference can't bake in a constant; it can only forward what the functional interface passes through. For c -> c.startsWith("WELCOME"), no method reference captures the "WELCOME" argument. A lambda is required there.

Cannot reference an instance method using an interface or abstract class's name if no concrete implementation exists in scope. List::size works because size is declared on List, and the receiver type is resolved later. List::new does not work, because List is an interface and can't be instantiated. The reference form requires a concrete class for ::new.

Parameterized type with `::new` has a syntactic quirk. Writing ArrayList<Product>::new with explicit type arguments on the class name in the reference is not legal. The legal forms are:

The first form is the everyday case: the diamond is inferred from the target type. The second form is legal but rare; the type argument sits on the class name on the left of ::. Most code uses the first form because the target type carries enough information.

What's wrong with this code?

The compiler reports something like incompatible types: incompatible parameter types in method reference. anchor::name is a bound reference and binds the specific anchor object. The resulting functional shape is "zero arguments, returns String", which fits Supplier<String>, not Function<Product, String>. The line tries to assign a zero-arity reference to a one-arity interface.

Fix: Use the unbound form by writing the class name on the left of ::.

Now the reference takes one Product argument, uses it as the receiver, and calls name(). The output is Pen. The mental switch is "a function that names any product" rather than "a function that names this specific product".

When to Use Method References and When to Prefer a Lambda

Method references are a stylistic choice, not a performance one. The compiler produces equivalent bytecode for the reference and the lambda it would replace. Pick whichever reads better at the call site.

Method references work well when:

  • The lambda body is a single method call with no transformation. s -> s.toLowerCase() reads worse than String::toLowerCase because the lambda repeats the parameter name unnecessarily.
  • The reference identifies a well-known method whose name carries meaning. Integer::parseInt and System.out::println are recognised on sight.
  • The reference participates in a chain of stream operations where adding lambda parameter names would clutter the chain.

Lambdas are clearer when:

  • The body does more than one call. s -> s.trim().toLowerCase() cannot be expressed as a single reference.
  • A constant value is involved. c -> c.startsWith("WELCOME") has the literal "WELCOME" baked in, which no reference can express.
  • The parameter name carries meaning that improves readability. price -> applyDiscount(price) may read better in context than MyClass::applyDiscount, even though both compile.
  • The method has many overloads and the target type doesn't make the correct choice obvious to the reader. Writing the lambda forces the choice to appear in the source.

The map step does two things: trim followed by toLowerCase. No method reference can express that without a helper method. A lambda is required here. Extracting a cleanName(String) helper just to enable a reference adds indirection for no gain in clarity. If cleanName were used in many places, the helper would be worth writing; otherwise, the lambda stays.

Quiz

Method References Quiz

10 quizzes