Method References

July 7, 20265 min readUpdated 8/20/2026

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:

FormWrittenMeans
Bound (2)prefix::concats -> prefix.concat(s) — receiver fixed, element is the argument
Unbound (3)String::toUpperCases -> 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::parseInt is 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.