Java Method References
Reference methods as lambdas in Java with the :: operator — static, instance, bound, and constructor 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 referenceThe 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)
List<String> log = new ArrayList<>();
Consumer<String> record = log::add; // msg -> log.add(msg)The bound receiver supplies the no-argument slot: because prefix is already captured, greet only needs the argument to concat, so it is a Function<String, String> rather than a BiFunction. Likewise log::add keeps log fixed and exposes only the element to add, giving a Consumer<String>.
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:
| Lambda | Method 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).
What to take from the run:
- All four forms compile to instances of ordinary functional interfaces —
parseis aFunction<String, Integer>whether you wrotes -> Integer.parseInt(s)orInteger::parseInt. The shorthand is purely syntactic. - The unbound
String::lengthandString::toUpperCaseboth have a receiver as the first parameter. That's whyString::lengthis aFunction<String, Integer>andString::startsWithis aBiPredicate<String, String>— the receiver is one slot, the explicit parameter the other. - The constructor reference
String[]::newproduced anIntFunction<String[]>— the form thatstream().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.