How to work with Method Reference — Java 21

pexels-photo-546819-546819.jpg

Method reference is a feature introduced in JDK 8 related to lambda expressions.

With a method reference, you can refer to a method without executing it. Just like a lambda expression, it requires a target type that consists of a compatible functional interface and, when evaluated, it creates an instance of the functional interface.

Method reference helps in making your code more concise and easier to read and improves your code expressiveness in functional-style programming.

Method reference to static method

You can use this syntax to create a static method reference:

ClassName::staticMethodName

You can use a method reference anywhere in which it is compatible with its target type (functional interface). Let’s see an example:

public class MethodRefDemo {

    public static void main(String[] args) {
        String outputString = stringOperation(MyStringOperation::strFirstLetterCapitalized, "abc");
        System.out.println(outputString);
    }

    static String stringOperation(StringFunction function, String str) {
        return function.strFunc(str);
    }

}

class MyStringOperation {
    public static String strFirstLetterCapitalized(String str) {
        char c = str.charAt(0);
        return String.valueOf(c).toUpperCase();
    }
}

interface StringFunction {
    String strFunc(String str);
}

The output is the capitalized first letter of the string:

A

Notice that strFirstLetterCapitalized is compatible with the StringFunction functional interface. That way, it can be passed any instance of that interface, including a method reference.

Method reference to instance method of a particular object

This is the syntax to pass a reference to an instance method to a specific object:

objReference::methodName

Here is the first program rewritten to use an instance method reference, which produces the same result as the previous one:

public class MethodRefDemo2 {

    public static void main(String[] args) {
        MyStringOperation myStringOperation = new MyStringOperation();
        String outputString = stringOperation(myStringOperation::strFirstLetterCapitalized, "abc");
        System.out.println(outputString);
    }

    static String stringOperation(StringFunction function, String str) {
        return function.strFunc(str);
    }

}

class MyStringOperation {
    public String strFirstLetterCapitalized(String str) {
        char c = str.charAt(0);
        return String.valueOf(c).toUpperCase();
    }
}

interface StringFunction {
    String strFunc(String str);
}

Notice that, in this program, strFirstLetterCapitalized() is now an instance method of MyStringOperation.

Method reference to instance method of an arbitrary object of a particular type

It’s also possible to specify an instance method that can be used with any object of a given class. This is the corresponding syntax:

ClassName::instanceMethodName

Let’s see an example:

import java.util.Arrays;
import java.util.List;

public class MethodRefDemo3 {

    public static void main(String[] args) {
        List<String> list = Arrays.asList("Jonas", "Ana", "Luna", "John");
        List<String> upperCaseList = list.stream()
                .map(String::toUpperCase)
                .toList();
    }

}

Here, String::toUpperCase is saying, “use the toUpperCase method of String objects”.

Method reference to a constructor

You can also create a reference to a constructor.

classname::new

You can assign this reference to any functional interface reference, as long as the it defines a method compatible with the constructor.

package org.example.methodreference;

public class MethodRefDemo4 {

    public static void main(String[] args) {
        MyFunction myFunction = MyClass::new;
        MyClass myClass = myFunction.func(10);
        System.out.println("Count is " + myClass.getCount());
    }

}

interface MyFunction {
    MyClass func(int count);
}

class MyClass {

    int count;

    MyClass() {
        this.count = 1;
    }

    MyClass(int count) {
        this.count = count;
    }

    public int getCount() {
        return count;
    }

}

The output is:

Count is 10

Let’s analyze what we just did:

1 — We created MyClass with two constructors, the default one and another one that receives an int argument;

2 — Then we created a functional interface Func with a method that has an int parameter and returns a MyClass type. Note that this abstract method is compatible with our second constructor, the one with a parameter;

3 — Finally, we created a main method inside MethodRefDemo4 class, where we assigned the reference method MyClass::new to a type MyFunction, and we called its functional method func passing the argument 10, creating an instance of MyClass (the implementation of the method func is our constructor). 

Constructor references to generic classes

A constructor reference to a generic class can be created pretty much the same way. Let’s rewrite the previous example using generics.

package org.example.methodreference;

public class MethodRefDemo5 {

    public static void main(String[] args) {
        MyFunction<Integer> myFunction = MyClass::new;
        MyClass<Integer> myClass = myFunction.func(10);
        System.out.println("Count is " + myClass.getCount());
    }

}

interface MyFunction<T> {
    MyClass<T> func(T count);
}

class MyClass<T> {

    T count;

    MyClass() {
        this.count = null;
    }

    MyClass(T count) {
        this.count = count;
    }

    T getCount() {
        return count;
    }

}

The output is:

Count is 10

Here, we made MyClass generic and the return type of the func method from the interface MyFunction, which is also generic, is MyClass<T>, making it compatible with one of the constructors.

Using method reference with a static factory

Now, let’s create a static method that is going to be a factory for objects of any type of MyFunctions objects.

package org.example.methodreference;

public class MethodRefDemo6 {

    static <R,T> R myClassFactory(MyFunction<R, T> cons, T v) {
        return cons.func(v);
    }

    public static void main(String[] args) {
        MyFunction<MyClass<Double>, Double> myClassCons = MyClass<Double>::new;
        MyClass<Double> myClass = myClassFactory(myClassCons, 123.45);
        System.out.println("val in myClass is " + myClass.getVal( ));

        MyFunction<MyClass2, String> myClassCons2 = MyClass2::new;
        MyClass2 myClass2 = myClassFactory(myClassCons2, "String Value");

        System.out.println("str in mc2 is " + myClass2.getVal());
    }
}

interface MyFunction<R, T> {
    R func(T value);
}

class MyClass<T> {

    private T val;

    MyClass(T val) {
        this.val = val;
    }

    MyClass() {
        this.val = null;
    }

    T getVal() {
        return val;
    }

}

class MyClass2 {

    String str;

    MyClass2(String s) {
        str = s;
    }

    MyClass2() {
        str = "";
    }

    String getVal() {
        return str;
    };

}

The output is:

val in myClass is 123.45
str in mc2 is String Value

1 — Here, we created two classes, MyClass (generic) and MyClass2 (non-generic), and the generic functional interface MyFunction;

2 — MyFunction has the abstract method func, that takes an argument of type T and return a type R;

3 — Then we created a static method that has two parameters, cons, which is of type MyFunction<R, T> and represents a reference to a constructor; and v of type T, which represents the argument passed to the constructor;

4 — Finally, in the main method of MethodRefDemo6, we created instances of MyClass and MyClass2 by creating a constructor reference of MyClass<Double> and another one for MyClass2, and passing the respective constructor reference along with the value (Double or String) to the static factory myClassFactory.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top