W3docs

Java Method References

Reference methods as lambdas in Java with the :: operator — static, instance, bound, and constructor references.

Java Method References

A method reference is a shorter syntax for a lambda whose body does nothing except call an existing method. When a lambda is literally x -> SomeClass.foo(x) or (a, b) -> a.bar(b), the :: operator lets you write it as SomeClass::foo or Some::bar. The compiler produces the same value either way — an instance of the appropriate functional interface — so wherever a lambda fits, a matching method reference fits too.

Function<String, Integer> len1 = s -> s.length();
Function<String, Integer> len2 = String::length;            // identical at runtime

List<String> names = List.of("Bob", "Alice");
names.forEach(s -> System.out.println(s));                   // lambda
names.forEach(System.out::println);                          // method reference

The four forms below cover every method reference you'll write. The single skill is recognising which form fits a given call site.

Form 1: Static method reference — ClassName::staticMethod

The method is a static method on a class. The reference becomes a lambda whose parameters are the static method's parameters:

Function<String, Integer> parse  = Integer::parseInt;        // s -> Integer.parseInt(s)
BinaryOperator<Integer>   max    = Math::max;                 // (a, b) -> Math.max(a, b)
Function<Object, String>  toStr  = String::valueOf;            // o -> String.valueOf(o)

This is the form that comes up in stream code like nums.stream().reduce(0, Integer::sum)Integer.sum(int, int) is static, so Integer::sum is a BinaryOperator<Integer> (a BiFunction<Integer, Integer, Integer>).

Form 2: Bound instance method reference — instance::method

The method is an instance method on a specific, named object. The reference becomes a lambda whose parameters are the method's parameters (the instance is captured):

PrintStream out = System.out;
Consumer<String> print = out::println;                  // s -> out.println(s)

String prefix = "Hello, ";
Function<String, String> greet = prefix::concat;        // name -> prefix.concat(name)

Map<String, Integer> scores = new HashMap<>();
Consumer<String> reset = scores::clear;                  // ignored input — clear() takes none
// Actually `scores::clear` is a Runnable; assignment to Consumer<String> would not compile.

The captured instance is held by the resulting object, similar to how a lambda captures effectively final locals. Bound references are the way you say "use this object's method as a callback" — Logger::info on a particular logger, event::handle on a particular handler.

Form 3: Unbound instance method reference — ClassName::method

The method is an instance method, but you reference it through the class rather than a specific instance. The reference becomes a lambda whose first parameter is the receiver, and the rest are the method's own parameters:

Function<String, Integer>   len    = String::length;             // s -> s.length()        — first param is the receiver
Function<String, String>    upper  = String::toUpperCase;         // s -> s.toUpperCase()
BiPredicate<String, String> starts = String::startsWith;          // (s, prefix) -> s.startsWith(prefix)

This is the form people stumble on. String::length looks like it might mean "the method length on the class String" — but there is no such static method. It really means "given any String, call its instance method length() — the receiver is the lambda's first parameter." That's why String::length is a Function<String, Integer> (one input, one output) and String::startsWith is a BiPredicate<String, String> (the second input is the prefix the receiver tests).

The form is the engine behind nearly every stream pipeline:

people.stream()
    .map(Person::name)               // unbound: p -> p.name()
    .filter(s -> s.startsWith("A"))
    .map(String::toUpperCase)        // unbound: s -> s.toUpperCase()
    .forEach(System.out::println);   // bound: s -> out.println(s)

Form 4: Constructor reference — ClassName::new

References a constructor as a function. The resulting lambda takes the constructor's parameters and returns a new instance:

Supplier<List<String>>           listOf  = ArrayList::new;                  // () -> new ArrayList<>()
Function<Integer, ArrayList<?>>  sized   = ArrayList::new;                   // n -> new ArrayList<>(n)
Function<String, BigDecimal>     toBig   = BigDecimal::new;                  // s -> new BigDecimal(s)
BiFunction<String, Integer, AbstractMap.SimpleEntry<String, Integer>> entry =
    AbstractMap.SimpleEntry::new;

Constructor references are how Collectors.toCollection(TreeSet::new) lets you pick a destination type, and how Stream.generate(Random::new) produces independent Random objects per call to get().

Arrays have a special form: String[]::new is a IntFunction<String[]>n -> new String[n]. This is what stream.toArray(String[]::new) uses.

Method reference vs lambda — when each wins

A method reference is the right call when the lambda's body is exactly a single method call with the parameters passed through in order:

LambdaMethod reference
s -> s.length()String::length
s -> System.out.println(s)System.out::println
(a, b) -> a.compareTo(b)String::compareTo
() -> new ArrayList<>()ArrayList::new

A lambda is the right call when the body does anything else:

  • Calls more than one method: s -> s.trim().toUpperCase() (no reference for the chain).
  • Has any argument transformation: s -> System.out.println("[" + s + "]").
  • Has any control flow: n -> n < 0 ? 0 : n.
  • Reorders or duplicates arguments: (a, b) -> b.compareTo(a) (reversed comparator).

The optimisation isn't really about runtime speed — both compile to the same invokedynamic bootstrap. It's about reading. Person::name jumps off the page as "the name field," whereas p -> p.name() makes you read three tokens. When the reference fits, prefer it; when it doesn't, don't twist the code to make it fit.

A constructor-reference pitfall: ambiguous overloads

ClassName::new works fine when there's one constructor matching the target interface. When there are several, the compiler picks based on the target type's parameter count and types. Mostly that works; occasionally it doesn't, and you need to disambiguate by typing the variable explicitly or falling back to a lambda:

// ArrayList has constructors: (), (int), (Collection)
Supplier<ArrayList<String>>           a = ArrayList::new;     // picks the no-arg
Function<Integer, ArrayList<String>>  b = ArrayList::new;     // picks the (int) one
Function<List<String>, ArrayList<String>> c = ArrayList::new; // picks the (Collection) one

// var inference can't disambiguate — this would not compile:
// var ambiguous = ArrayList::new;

The fix is to keep the target type explicit, as in a, b, c above.

A worked example: all four forms in one program

The program below builds and uses one method reference of each form, demonstrates how String::length (unbound) becomes a Function<String, Integer>, and shows the constructor-reference trick that drives stream().toArray(T[]::new).

java— editable, runs on the server

What to take from the run:

  • All four forms compile to instances of ordinary functional interfaces — parse is a Function<String, Integer> whether you wrote s -> Integer.parseInt(s) or Integer::parseInt. The shorthand is purely syntactic.
  • The unbound String::length and String::toUpperCase both have a receiver as the first parameter. That's why String::length is a Function<String, Integer> and String::startsWith is a BiPredicate<String, String> — the receiver is one slot, the explicit parameter the other.
  • The constructor reference String[]::new produced an IntFunction<String[]> — the form that stream().toArray(...) wants. Constructor references are how you tell a stream "here's the destination type."
  • The reversed-length comparator could not be written as a method reference: the receiver and the parameter swap, and method references can't reorder arguments. That's exactly the kind of case where a lambda is still the right call.

What's next

You can now write a stream pipeline almost entirely in method references and let the few transformations that genuinely need shaping live in small lambdas. That style is the natural lead-in to the part's centrepiece: streams. The next chapter, Java Streams Introduction, introduces the Stream<T> API — what it is, what a stream pipeline looks like, why it's lazy, why it can only be used once, and how it fits together with the lambdas, functional interfaces, and method references you've just learned.

Practice

Practice

`String::length` is which kind of method reference, and what functional interface does it match?