Method References
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 callsIf anything is added, reordered, or wrapped, the lambda stays.
The four forms
Method reference forms
| Form | Syntax | Equivalent lambda |
|---|---|---|
| Static method | Integer::parseInt | s -> Integer.parseInt(s) |
| Method of a specific object | System.out::println | x -> System.out.println(x) |
| Method of an arbitrary instance | String::length | s -> s.length() |
| Constructor | ArrayList::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 clearerNeither 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::methodfixes the object;Type::methodmakes 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::newwhere no matching constructor exists. - Reaching for references reflexively even when they obscure the intent.
Practice
- Rewrite
list.forEach(s -> System.out.println(s))as a method reference. - Convert a
List<String>of digits into aList<Integer>using a method reference. - Sort a list of your own objects by an integer field using
Comparator.comparingIntand a method reference. - Use a constructor reference to create a
Supplierthat produces emptyArrayListobjects, and call it twice — confirm you get two distinct lists. - Write a lambda that cannot become a method reference, and explain in a comment why not.
Hints
System.out::println..map(Integer::parseInt)on a stream, or a loop calling it.Comparator.comparingInt(Motor::getPort).Supplier<List<String>> s = ArrayList::new;then callget()twice and compare with==— they should differ.- Anything with an extra argument, reordered parameters, or arithmetic around the call.
x -> compute(x, 10)will do.