A method reference is a lambda with the noise removed. When a lambda does nothing but call one
existing method, :: lets you name that method instead of describing the call.
List<String> names = List.of("ana", "bo");
names.stream().map(s -> s.toUpperCase()).toList(); // lambda
names.stream().map(String::toUpperCase).toList(); // method reference — identical
They compile to the same thing — the method reference is not faster, and it is not a different mechanism. It is the same lambda with the parameter name and the call syntax removed, which is worth doing only when that removal makes the line easier to read.
This post covers the four forms, because the third one looks wrong the first time you meet it, and then the rule for choosing between a reference and a lambda.
1. Static method
List<String> numbers = List.of("1", "2", "3");
System.out.println(numbers.stream().map(s -> Integer.parseInt(s)).toList()); // lambda
System.out.println(numbers.stream().map(Integer::parseInt).toList()); // [1, 2, 3]
System.out.println(List.of(1, 2, 3).stream().reduce(0, Integer::sum)); // 6
ClassName::staticMethod. The argument becomes the method's argument. This is the
straightforward case.
2. Instance method of a particular object
List<String> names = List.of("Ana", "Bo");
names.forEach(n -> System.out.println(n)); // lambda
names.forEach(System.out::println); // bound to the System.out object
String prefix = "Order-";
System.out.println(names.stream().map(prefix::concat).toList()); // [Order-Ana, Order-Bo]
object::method. The object is fixed — System.out here — and the stream
element becomes the argument. This is called a bound reference because the receiver is
already decided.
3. Instance method of an arbitrary object
This is the one that confuses people:
List<String> names = List.of("Christopher", "Bo");
System.out.println(names.stream().map(s -> s.toUpperCase()).toList()); // lambda
System.out.println(names.stream().map(String::toUpperCase).toList()); // [CHRISTOPHER, BO]
System.out.println(names.stream().map(String::length).toList()); // [11, 2]
System.out.println(names.stream().sorted(String::compareToIgnoreCase).toList());
String::toUpperCase looks like a static call, and toUpperCase is not
static. The rule: the first argument becomes the receiver.
String::toUpperCase means "take a String and call toUpperCase on it" — the
stream element is not passed to the method, it is the object the method is called
on.
Compare with form 2 and the difference is which part is fixed:
| Form | Written | Means |
|---|---|---|
| Bound (2) | prefix::concat | s ->
prefix.concat(s) — receiver fixed, element is the argument |
| Unbound (3) | String::toUpperCase | s ->
s.toUpperCase() — element is the receiver |
With a two-argument functional interface, the first becomes the receiver and the second the argument:
BiFunction<String, String, Boolean> starts = String::startsWith;
System.out.println(starts.apply("hello world", "hello")); // true — "hello world".startsWith("hello")
4. Constructor
record Person(String name) { }
class Demo {
void run() {
List<String> names = List.of("Ana", "Bo");
System.out.println(names.stream().map(n -> new Person(n)).toList()); // lambda
System.out.println(names.stream().map(Person::new).toList()); // constructor ref
Supplier<List<String>> maker = ArrayList::new;
System.out.println(maker.get().size()); // 0
List<String> copy = names.stream()
.collect(Collectors.toCollection(ArrayList::new));
System.out.println(copy.size()); // 2
}
}
ClassName::new. The compiler picks the constructor whose parameters match the
functional interface, so Person::new works for a one-argument Function and
ArrayList::new works for a no-argument Supplier.
When a method reference is clearer
The test is narrow and worth applying literally: use a method reference when the lambda's entire body is one call and the arguments pass straight through, unchanged and in order.
List<String> names = List.of("ana", "bo");
// Yes — pure pass-through
names.stream().map(String::toUpperCase).toList();
names.forEach(System.out::println);
// No — the argument is transformed first, so a reference cannot express it
names.stream().map(s -> s.toUpperCase() + "!").toList();
// No — the arguments are reordered
BiFunction<String, String, Boolean> reversed = (a, b) -> b.startsWith(a);
System.out.println(reversed.apply("he", "hello")); // true
The point is readability, not brevity for its own sake. String::toUpperCase is easier
to read than s -> s.toUpperCase() because there is no invented variable name to
follow. But a method reference that forces the reader to work out which form it is has cost more than
it saved.
One in context
Both an unbound reference and a static-style one, in three lines of the console bank app this site uses for examples:
return accountsOf(user).stream()
.map(Account::balance)
// reduce folds the list into one value: start at zero, add each balance.
// BigDecimal::add is a method reference standing in for (a, b) -> a.add(b).
.reduce(Money.ZERO, BigDecimal::add);
Account::balance is form 3 — each account becomes the receiver.
BigDecimal::add is form 3 with two arguments: the accumulator becomes the receiver and
the next balance becomes the argument, exactly as the String::startsWith example
above.
Comparators, where they pay off most
Sorting is where method references genuinely transform readability, because
Comparator's factory methods are built to take them:
record Person(String name, String city, int age) { }
class Demo {
void run() {
List<Person> people = new ArrayList<>(List.of(
new Person("Cy", "Seattle", 35),
new Person("Ana", "Nuku'alofa", 30),
new Person("Bo", "Seattle", 30)));
people.sort(Comparator.comparing(Person::name));
people.sort(Comparator.comparingInt(Person::age).reversed());
// Sort by age, then break ties by name
people.sort(Comparator.comparingInt(Person::age).thenComparing(Person::name));
System.out.println(people.stream().map(Person::name).toList()); // [Ana, Bo, Cy]
// Nulls last, without writing the null checks yourself
people.sort(Comparator.comparing(Person::city, Comparator.nullsLast(Comparator.naturalOrder())));
}
}
Written out as lambdas, that thenComparing chain becomes a multi-line
compare method with an if in it. The method-reference version says what the
sort key is and nothing else — and it is the form you will see in nearly all modern Java.
What they cannot do
Two limits worth knowing before you go looking for a syntax that does not exist:
- No arguments can be supplied.
Integer::parseIntis fine; "parseInt with radix 16" is not expressible — write the lambda. - No overload disambiguation. If two overloads both fit the functional interface, the reference is ambiguous and the compiler says so. A lambda with explicit parameter types resolves it.
There is also a common surprise: this::method and super::method are both
legal and useful inside a class, referring to the current object's method — the bound form from
section 2, with this as the fixed receiver.
Extracting a method to get one
The best use of method references is often to give a piece of logic a name:
record Order(String id, double total, boolean paid) { }
class Report {
// A predicate with a name is documentation the compiler checks
static boolean isLargeUnpaid(Order o) {
return !o.paid() && o.total() > 100;
}
void run() {
List<Order> orders = List.of(
new Order("a", 250, false),
new Order("b", 50, false));
// Inline: the reader has to parse the condition
System.out.println(orders.stream().filter(o -> !o.paid() && o.total() > 100).count());
// Named: the reader gets the intent
System.out.println(orders.stream().filter(Report::isLargeUnpaid).count()); // 1
}
}
This also makes the condition testable on its own and reusable in the three other places that will eventually need it.
Next
findFirst() returned an Optional back in
Streams without explanation.
Optional is next.