Method References
Replace lambdas that only call an existing method with Type::method, and know the four kinds and the one place they behave differently.
ClassName::staticMethod
instance::instanceMethod
ClassName::instanceMethod
ClassName::newA method reference is a lambda whose whole body is a call to a method that already exists. s -> s.strip() becomes String::strip. It produces the same functional interface instance; it just names the method instead of restating it. Use one when the name says more than the lambda would.
The four kinds
| Kind | Reference | Equivalent lambda |
|---|---|---|
| Static method | Integer::parseInt | s -> Integer.parseInt(s) |
| Instance method of a particular object (bound) | prefix::concat | s -> prefix.concat(s) |
| Instance method of any object of a type (unbound) | String::toUpperCase | s -> s.toUpperCase() |
| Constructor | ArrayList::new | () -> new ArrayList<>() |
With an unbound reference the first parameter becomes the receiver. That is why String::compareTo fits a Comparator<String>: (a, b) -> a.compareTo(b). Which constructor or overload is picked depends on the target type, so Tag::new as a Function<String, Tag> calls the one-argument constructor.
Where they read well
Comparators (Comparator.comparing(Order::createdAt)), cleaning input (names.replaceAll(String::strip)), dropping blanks (lines.removeIf(String::isBlank)), and factories (Supplier<Map<String, Integer>> f = TreeMap::new). In services, a bound reference such as repository::findById hands a dependency's method to code that only needs a Function.
Mistakes people make
- A null receiver fails early. In
obj::methodthe receiver is evaluated when the reference is created. Ifobjis null you get aNullPointerExceptionright there. The lambda() -> obj.method()only fails when it is called. - Bound to stale state. A bound reference keeps the object it was created with. If you later assign a new object to the variable, the reference still points at the old one.
- Forcing it. When arguments need reordering, adding or a constant, as in
s -> s.substring(1), a lambda is clearer. A method reference never takes extra arguments. - Constructor shape mismatch.
map.computeIfAbsent(key, ArrayList::new)does not compile for aStringkey, because the function receives the key and noArrayListconstructor takes aString. Writek -> new ArrayList<>().
Pointing at a named method also helps with debugging and testing: stack traces show the method's real name rather than a generated lambda$main$0, and the method can be unit tested on its own.
Example
When to use it
- Sort API results with Comparator.comparing(Order::createdAt) instead of a hand-written compare lambda.
- Clean incoming tag lists with removeIf(String::isBlank) and replaceAll(String::strip) before saving them.
- Pass repository::findById to a caching helper that only needs a Function from id to entity.
More examples
Bound versus unbound
The same String::concat method fits a Function when the receiver is supplied up front and a BiFunction when it is not. String::compareTo fits Comparator because the first parameter becomes the receiver.
void main() {
String prefix = "/api/v1";
// Bound: the receiver is fixed, the function takes one argument.
Function<String, String> withPrefix = prefix::concat;
IO.println(withPrefix.apply("/orders"));
// Unbound: the first argument becomes the receiver.
BiFunction<String, String, String> join = String::concat;
IO.println(join.apply("/api/v2", "/orders"));
Comparator<String> natural = String::compareTo;
IO.println(natural.compare("alpha", "beta") < 0);
}
// Output:
// /api/v1/orders
// /api/v2/orders
// trueA null receiver fails at creation
The receiver of a bound reference is checked for null as soon as the reference is made. That is useful for failing fast at start-up, and surprising if you expected deferred behaviour.
void main() {
String region = null;
try {
Supplier<Integer> length = region::length;
IO.println("reference created");
} catch (NullPointerException e) {
IO.println("method reference failed at creation");
}
Supplier<Integer> lazy = () -> region.length();
IO.println("lambda created");
try {
lazy.get();
} catch (NullPointerException e) {
IO.println("lambda failed when called");
}
}
// Output:
// method reference failed at creation
// lambda created
// lambda failed when calledConstructor references as factories
The target type decides which constructor a ::new reference uses. Passing a Supplier of a map lets a caller choose TreeMap for sorted output without the code that fills it knowing.
record Tag(String name) {}
void main() {
Function<String, Tag> toTag = Tag::new; // picks the one-argument constructor
List<Tag> tags = new ArrayList<>();
for (String name : List.of("java", "api", "http")) {
tags.add(toTag.apply(name));
}
tags.sort(Comparator.comparing(Tag::name));
IO.println(tags);
Supplier<Map<String, Integer>> newCounts = TreeMap::new; // picks the no-argument constructor
Map<String, Integer> counts = newCounts.get();
counts.put("b", 2);
counts.put("a", 1);
IO.println(counts);
}
// Output:
// [Tag[name=api], Tag[name=http], Tag[name=java]]
// {a=1, b=2}
Discussion