Java Method Reference

Method reference lets you use an existing method as a lambda as long as their signature (input & output) are compatible.

Gerald Nguyen
Gerald Nguyen
4 min read ·
Previous | Next
Also on Medium
On this page

I first learned method reference from C#, so the Java concept is familiar to me. Or that’s what I thought until I picked up the Modern Java Recipes book.

In this article, we’ll explore 3 forms of method references in Java

The last one is a bit more special, as we shall examine in detail

Source: author

Source: author

object::instanceMethod

The object::instanceMethod will invoke the referenced method on the said object with compatible arguments. In the following example, the "hello " string’s concat method is invoked with arguments "a", "b", "c" resulting in "hello a", "hello b", "hello c" respectively

@Test  
void invoke_the_method_on_the_owning_object() {  
    var aList = List.of("a", "b", "c");  
    assertEquals(  
        List.of("hello a", "hello b", "hello c"),  
        aList.stream().map("hello "::concat).collect(Collectors.toList())  
    );  
}

The holder of the method reference is responsible to invoke it with compatible method arguments. In the following example, we test object::instanceMethod with zero, one, two, or more parameters (using a custom functional interface). As long as the number and types of arguments are compatible with the instance method’s signature, the codes compile and run correctly.

private String noParam(Supplier<String> supplier) {  
    return supplier.get();  
}  
private String oneParam(Function<String, String> function) {  
    return function.apply("1");  
}  
private String twoParam(BiFunction<Integer, Integer, String> function) {  
    return function.apply(1, 2);  
}  
private LocalDateTime threeParam(ThreeParam1 function) {  
    return function.apply(1, 2, 3);  
}  
  
interface ThreeParam1 {  
    LocalDateTime apply(int one, int two, int three);  
}  
  
@Test  
void invoke_with_compatible_arguments() {  
    // like a Supplier  
    assertEquals("a", noParam("a "::trim));  
    // like a Function or Consumer  
    assertEquals("a1", oneParam("a"::concat));  
    // like a BiFunction or BiConsumer  
    assertEquals("1", twoParam("0123"::substring));  
    // or any compatible Functional Interface  
    assertEquals(  
        LocalDateTime.of(2021, 10, 11, 1, 2, 3),  
        threeParam(LocalDate.of(2021, 10, 11)::atTime));  
}

Class::staticMethod

The Class::staticMethod will invoke the referenced method on the declared class with compatible arguments. In the following example, String class’ static method valueOf is invoked with arguments 1, 2.0, true resulting in "1", "2.0", "true" string instances respectively

@Test  
void invoke_the_static_method_on_the_class() {  
    var aList = List.of(1, 2.0, true);  
    assertEquals(  
        List.of("1", "2.0", "true"),  
        aList.stream().map(String::valueOf).collect(Collectors.toList())  
    );  
}

The holder of the method reference is responsible to invoke it with compatible method arguments. In the following example, we test Class::staticMethod with zero, one, two, or more parameters (using a custom functional interface). As long as the number and types of arguments are compatible with the instance method’s signature, the codes compile and run correctly.

private String noParam(Supplier<String> supplier) {  
    return supplier.get();  
}  
private String oneParam(Function<String, String> function) {  
    return function.apply("1");  
}  
private String twoParam(BiFunction<String, String, String> function) {  
    return function.apply("%s", "1");  
}  
private LocalDate threeParam(ThreeParam3 function) {  
    return function.apply(1, 2, 3);  
}  
  
interface ThreeParam3 {  
    LocalDate apply(int one, int two, int three);  
}  
  
@Test  
void invoke_with_compatible_arguments() {  
    // like a Supplier  
    assertEquals(System.lineSeparator(), noParam(System::lineSeparator));  
    // like a Function or Consumer  
    assertEquals("1", oneParam(String::valueOf));  
    // like a BiFunction or BiConsumer  
    assertEquals("1", twoParam(String::format));  
    // or any compatible Functional Interface  
    assertEquals(  
        LocalDate.of(1, 2, 3),  
        threeParam(LocalDate::of));  
}

Class::instanceMethod

The Class::instanceMethod will invoke the referenced method on, of course, an instance of the said class. As I said earlier, this syntax is a bit special, because it isn’t clear what is that instance and who will provide it

Fortunately, the below example shed some light on that mystery. Let’s see how the String::uppercase method invocation gets its instance

@Test  
void invoke_the_method_on_the_class_instance() {  
    var aList = List.of("a", "b", "c");  
    assertEquals(  
        aList.stream().map(a -> a.toUpperCase()).collect(Collectors.toList()),  
        aList.stream().map(String::toUpperCase).collect(Collectors.toList())  
    );  
}

It appears the JVM has a hand in this mystery. When map receives an object instance where Class::instanceMethod is specified, the JVM invokes that method (e.g. toUpperCase()) on the said object (e.g. "a")

There is a catch, however. toUpperCase() takes in no parameter while thismap(...) expects a Function<String, String> with one parameter. It turns out that the functional interface has an extra parameter of the same type of declared class. As we established before, the JVM will then invoke the referenced method on that parameter.

In the following example, each Class::instanceMethod is invoked with an extra first argument of the same type as the declared class. The remaining arguments (if there are any) must be compatible with their signature

private String noParam(Supplier<String> supplier) {  
    return supplier.get();  
}  
private String oneParam(Function<String, String> function) {  
    return function.apply("1   ");  
}  
private String twoParam(BiFunction<String, String, String> function) {  
    return function.apply("1", "2");  
}  
private LocalDateTime threeParam(ThreeParam2 function) {  
    return function.apply(LocalDate.of(2021, 10, 11), 2, 3);  
}  
  
interface ThreeParam2 {  
    LocalDateTime apply(LocalDate one, int two, int three);  
}  
  
@Test  
void invoke_with_compatible_arguments() {   // as long as the 1st param is a class's instance  
    // like a Supplier                                  // Constructor is neither static nor instance  
    assertEquals("", noParam(String::new));     // including here just to complete 0, 1, 2, 3 params  
    // like a Function or Consumer  
    assertEquals("1", oneParam(String::trim));  
    // like a BiFunction or BiConsumer  
    assertEquals("12", twoParam(String::concat));  
    // or any compatible Functional Interface  
    assertEquals(  
        LocalDateTime.of(2021, 10, 11, 2, 3),  
        threeParam(LocalDate::atTime)); // public LocalDateTime atTime(int hour, int minute)  
}

Checkout the code from https://github.com/geraldnguyen/kitchensink/blob/main/java/src/test/java/core/MethodReferenceTest.java