skip to content
everything.functional.methodReferences

method references

the four kinds of ::, and what each one desugars to.

chapter
chapter 07.03
reading time
5 min read

a method reference like String::length is a lambda whose whole body is one call to a method that already exists, written as a name instead of a body.

there are four kinds: static (Integer::parseInt), bound (System.out::println), unbound (String::length) and constructor (ArrayList::new). the target type decides how the arguments line up.

the picture

you know how half your lambdas end up as x -> something(x)? the lambda adds nothing except a name for the argument, which you immediately hand to someone else. a method reference skips the middleman.

think of a speed dial. instead of writing out the number every time (the lambda body), you point at a contact that's already saved (the method). the phone knows how to dial it.

kind =
Class::instanceMethodthere's no receiver yet. the first argument becomes the receiver, the rest become arguments.
String::length≡s -> s.length()
asFunction<String, Integer>the one argument is the String to call length() on.
String::compareToIgnoreCase≡(a, b) -> a.compareToIgnoreCase(b)
asComparator<String>two arguments: the first is the receiver, the second is the argument. that's why it fits Comparator.
1st argument2nd argument
pick a kind. each method reference sits next to the lambda it stands for. amber is the first argument the interface hands you, so watch where it goes: straight into the call for static, into the receiver position for unbound. blue is a receiver captured when the reference is created.

all four, running

Main.java
import java.util.*;
import java.util.function.*;
 
void main() {
    // 1. static: Class::staticMethod
    Function<String, Integer> parse = Integer::parseInt;   // s -> Integer.parseInt(s)
    // 2. bound: expression::method, the receiver is fixed right now
    String greeting = "hello";
    Predicate<String> starts = greeting::startsWith;   // p -> greeting.startsWith(p)
    // 3. unbound: Class::instanceMethod, the first argument is the receiver
    Function<String, Integer> len = String::length;    // s -> s.length()
    Comparator<String> cmp = String::compareToIgnoreCase;
                                         // (a, b) -> a.compareToIgnoreCase(b)
    // 4. constructor: Class::new (arrays too)
    Supplier<List<String>> fresh = ArrayList::new;     // () -> new ArrayList<>()
    IntFunction<int[]> array = int[]::new;             // n -> new int[n]
 
    IO.println(parse.apply("42") + 1);
    IO.println(starts.test("hell"));
    IO.println(len.apply("duke"));
    IO.println(cmp.compare("JAVA", "java"));
    IO.println(fresh.get());
    IO.println(array.apply(3).length);
}
output
43
true
4
0
[]
3

the same :: text can mean different things depending on the target. String::compareToIgnoreCase takes one argument as a method, but as a Comparator<String> it takes two: the first one becomes the object the method is called on. that's the unbound trick, and it's why list.sort(String::compareToIgnoreCase) works.

sort them yourself

the only hard part is telling bound from unbound. the question to ask: is there an actual object to the left of ::, or just a class name?

String::toUpperCase
Integer::sum
list::add
StringBuilder::new
Object::toString
this::render
List::of
String[]::new
0/8 sorted
pick a kind for each one. a class name before an instance method is unbound, an expression (a variable, this, a call) is bound, ::new is always a constructor.

when the receiver gets evaluated

here's the subtle difference between a bound reference and the lambda you'd write by hand. the lambda evaluates sb every time it runs. the method reference evaluates sb once, when the reference is created, and keeps that object.

predict the output
java
StringBuilder sb = new StringBuilder("a");
Supplier<String> snap = sb::toString;
sb = new StringBuilder("b");
IO.println(snap.get());

that also means a null receiver blows up early, at creation, not when you call it:

Main.java
import java.util.function.*;
 
void main() {
    String name = null;
    Supplier<Integer> lazy = () -> name.length();   // fine until get()
    IO.println("lambda created");
    Supplier<Integer> eager = name::length;          // throws right here
    IO.println("method ref created");
}
output
lambda created
Exception in thread "main" java.lang.NullPointerException

javac literally inserts an Objects.requireNonNull(name) before the invokedynamic, which is why the stack trace points at Objects.requireNonNull and you get no helpful message about which variable was null.

when it's ambiguous

a method reference has to resolve to exactly one method. overloads can make that impossible:

java
Function<Integer, String> f = Integer::toString;
text
error: incompatible types: invalid method reference
    reference to toString is ambiguous
      both method toString(int) in Integer and method toString() in Integer match

as a Function<Integer, String>, the Integer argument could be the receiver of toString() (unbound) or the argument to static toString(int). javac refuses to guess. write the lambda (i -> i.toString()), or pick a reference with only one reading, like Object::toString or String::valueOf.

one level deeper:what javac emits for a method reference+

same invokedynamic machinery as a lambda, minus one step. for x -> x.length() javac has to write a private lambda$main$0 method to hold the body. for String::length there's nothing to write: the bootstrap method gets a direct handle to the method you named.

text
REF_invokeVirtual java/lang/String.length:()I       // String::length
REF_invokeStatic  java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;

for a bound reference, the receiver is evaluated, null-checked with Objects.requireNonNull, and passed to the invokedynamic as a captured argument, exactly like a captured local in a lambda. more in lambda internals.

keylines
  • a method reference is a lambda that only calls one method. same objects, same invokedynamic.
  • static: arguments pass straight through. constructor: ::new, arrays included.
  • unbound (Class::method): the first argument becomes the receiver.
  • bound (expr::method): the receiver is evaluated once, at creation. null fails right there.
  • overloads can make a reference ambiguous. then just write the lambda.