Method References

intermediate25 min

When a lambda just forwards

Some lambdas do nothing but call an existing method with the same arguments:

names.forEach(name -> System.out.println(name));

The lambda takes name and passes it straight to println. The parameter adds nothing — it exists only to be handed along.

A method reference names the method directly:

names.forEach(System.out::println);

Same behaviour, and the noise is gone. This is sugar over a lambda, which is itself sugar over an anonymous class — three layers of shorthand for the same underlying object.

The test for using one

Use a method reference when the lambda’s entire body is a single call whose arguments are exactly the lambda’s parameters, in the same order.

x -> foo(x)              ->  becomes a reference
(a, b) -> foo(a, b)      ->  becomes a reference

x -> foo(x, 10)          ->  keep the lambda: extra argument
x -> foo(x) + 1          ->  keep the lambda: extra work
(a, b) -> foo(b, a)      ->  keep the lambda: order swapped
x -> foo(bar(x))         ->  keep the lambda: nested calls

If anything is added, reordered, or wrapped, the lambda stays.

The four forms

Method reference forms

FormSyntaxEquivalent lambda
Static methodInteger::parseInts -> Integer.parseInt(s)
Method of a specific objectSystem.out::printlnx -> System.out.println(x)
Method of an arbitrary instanceString::lengths -> s.length()
ConstructorArrayList::new() -> new ArrayList<>()

The third form is the one that confuses people, so it is worth slowing down on.

The arbitrary-instance form

Compare these two:

String prefix = "drive";
Predicate<String> a = prefix::startsWith;   // specific object
Function<String, Integer> b = String::length; // arbitrary instance

In the first, prefix is a particular object. Calling the predicate with "x" runs prefix.startsWith("x") — the object is fixed, the argument varies.

In the second, no object is named. Calling the function with "hello" runs "hello".length() — the argument becomes the object the method is called on.

So String::length describes a function from a String to its length. The first parameter becomes the receiver.

List<String> names = List.of("intake", "drive", "shooter");

names.stream()
     .map(String::length)          // each name becomes its length
     .forEach(System.out::println);

Constructor references

Useful where something needs to create objects without knowing which type:

Supplier<List<String>> maker = ArrayList::new;
List<String> fresh = maker.get();          // a new empty ArrayList

With an argument, it maps to a matching constructor:

class Motor {
    private final int port;
    Motor(int port) { this.port = port; }
}

Function<Integer, Motor> motorFactory = Motor::new;
Motor m = motorFactory.apply(3);           // calls new Motor(3)

Which constructor gets used is decided by the functional interface’s shape. A Supplier needs a no-argument constructor; a Function needs a one-argument one. If no constructor matches, it will not compile.

Where they read well

// parsing a list of strings into numbers
List<Integer> numbers = raw.stream()
                           .map(Integer::parseInt)
                           .toList();

// sorting by a field
motors.sort(Comparator.comparingInt(Motor::getPort));

// filtering out blanks
names.removeIf(String::isBlank);

// running an existing method as a thread body
new Thread(this::periodic).start();

That last one is common in robot code — this::periodic refers to a method on the current object, which is the “specific object” form with this as the object.

Where they read badly

A method reference is only clearer when the method name explains itself. When the target is obscure, the explicit lambda is easier to follow:

list.forEach(Helper::process);         // process what? how?
list.forEach(item -> Helper.process(item));   // marginally clearer

Neither is really good — the fix is a better method name. Do not treat method references as automatically superior; the goal is a reader understanding the code quickly.

Ambiguity

Occasionally the compiler cannot tell which overload you mean:

// if a class has both process(String) and process(int),
// Helper::process may be ambiguous depending on the target type

When that happens, write the lambda instead — the argument types make it explicit. This is uncommon, but the error message can be cryptic, so it helps to know the cause.

Common mistakes

  • Using :: when the lambda does extra work. Only a pure forwarding call qualifies.
  • Confusing the two instance forms. obj::method fixes the object; Type::method makes the first argument the object.
  • Expecting a reference to a method with the wrong number of parameters to work. The shape must match the functional interface.
  • Writing Type::new where no matching constructor exists.
  • Reaching for references reflexively even when they obscure the intent.

Practice

  1. Rewrite list.forEach(s -> System.out.println(s)) as a method reference.
  2. Convert a List<String> of digits into a List<Integer> using a method reference.
  3. Sort a list of your own objects by an integer field using Comparator.comparingInt and a method reference.
  4. Use a constructor reference to create a Supplier that produces empty ArrayList objects, and call it twice — confirm you get two distinct lists.
  5. Write a lambda that cannot become a method reference, and explain in a comment why not.
Hints
  1. System.out::println.
  2. .map(Integer::parseInt) on a stream, or a loop calling it.
  3. Comparator.comparingInt(Motor::getPort).
  4. Supplier<List<String>> s = ArrayList::new; then call get() twice and compare with == — they should differ.
  5. Anything with an extra argument, reordered parameters, or arithmetic around the call. x -> compute(x, 10) will do.

Next

Related