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.

SyntaxClassName::staticMethod instance::instanceMethod ClassName::instanceMethod ClassName::new

A 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

KindReferenceEquivalent lambda
Static methodInteger::parseInts -> Integer.parseInt(s)
Instance method of a particular object (bound)prefix::concats -> prefix.concat(s)
Instance method of any object of a type (unbound)String::toUpperCases -> s.toUpperCase()
ConstructorArrayList::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::method the receiver is evaluated when the reference is created. If obj is null you get a NullPointerException right 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 a String key, because the function receives the key and no ArrayList constructor takes a String. Write k -> 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · java
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
// true

A 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.

Example · java
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 called

Constructor 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.

Example · java
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

  • Be the first to comment on this lesson.