Use method references to simplify functional programming in Java
Along with lambdas, Java SE 8 brought method references to the Java language. This tutorial offers a brief overview of method references in Java, then gets you started using them with Java code examples. By the end of the tutorial you will know how to use method references to refer to a class’s static methods, bound and unbound non-static methods, and constructors, as well as how to use them to refer to instance methods in superclass and current class types. You’ll also understand why many Java developers have adopted lambda expressions and method references as a cleaner, simpler alternative to anonymous classes.
Note that code examples in this tutorial are compatible with JDK 12.
Method references: A primer
My previous Java 101 tutorial introduced lambda expressions, which are used to define anonymous methods that can then be treated as instances of a functional interface. Sometimes, a lambda expression does nothing more than call an existing method. For example, the following code fragment uses a lambda to invoke System.out
‘s void println(s)
method on the lambda’s single argument–s
‘s type is not yet known:
(s) -> System.out.println(s)
The lambda presents (s)
as its formal parameter list and a code body whose System.out.println(s)
expression prints s
‘s value to the standard output stream. It doesn’t have an explicit interface type. Instead, the compiler infers from the surrounding context which functional interface to instantiate. For example, consider the following code fragment:
Consumer<String> consumer = (s) -> System.out.println(s);
The compiler analyzes the previous declaration and determines that the java.util.function.Consumer
predefined functional interface’s void accept(T t)
method matches the lambda’s formal parameter list ((s)
). It also determines that accept()
‘s void
return type matches println()
‘s void
return type. The lambda is thus bound to Consumer
.
More specifically, the lambda is bound to Consumer<String>
. The compiler generates code so that an invocation of Consumer<String>
‘s void accept(String s)
method results in the string argument passed to s
being passed to System.out
‘s void println(String s)
method. This invocation is shown below:
consumer.accept("Hello"); // Pass "Hello" to lambda body. Print Hello to standard output.
To save keystrokes, you can replace the lambda with a method reference, which is a compact reference to an existing method. For example, the following code fragment replaces (String s) -> System.out.println(s)
with System.out::println
, where ::
signifies that System.out
‘s void
println(String s)
method is being referenced:
Consumer<String> consumer2 = System.out::println; // The method reference is shorter.
consumer2.accept("Hello"); // Pass "Hello" to lambda body. Print Hello to standard output.
It isn’t necessary to specify a formal parameter list for the previous method reference because the compiler can infer this list based on Consumer<String>
This parameterized type’s java.lang.String
actual type argument replaces T
in void accept(T t)
, and is also the type of the single parameter in the lambda body’s System.out.println()
method call.
Method references in depth
A method reference is a syntactic shortcut for creating a lambda from an existing method. Instead of providing an implementation body, a method reference refers to an existing class’s or object’s method. As with a lambda, a method reference requires a target type.
You can use method references to refer to a class’s static methods, bound and unbound non-static methods, and constructors. You can also use method references to refer to instance methods in superclass and current class types. I’ll introduce you to each of these method reference categories and show how they’re used in a small demo.
References to static methods
A static method reference refers to a static method in a specific class. Its syntax is className::staticMethodName
, where className
identifies the class and staticMethodName
identifies the static method. An example is Integer::bitCount
. Listing 1 demonstrates a static method reference.
Listing 1. MRDemo.java (version 1)
import java.util.Arrays;
import java.util.function.Consumer;
public class MRDemo
{
public static void main(String[] args)
{
int[] array = { 10, 2, 19, 5, 17 };
Consumer<int[]> consumer = Arrays::sort;
consumer.accept(array);
for (int i = 0; i < array.length; i++)
System.out.println(array[i]);
System.out.println();
int[] array2 = { 19, 5, 14, 3, 21, 4 };
Consumer<int[]> consumer2 = (a) -> Arrays.sort(a);
consumer2.accept(array2);
for (int i = 0; i < array2.length; i++)
System.out.println(array2[i]);
}
}
Listing 1’s main()
method sorts a pair of integer arrays via the java.util.Arrays
class’s static void sort(int[] a)
method, which appears in static method reference and equivalent lambda expression contexts. After sorting an array, a for
loop prints the sorted array’s contents to the standard output stream.
Before we can use a method reference or a lambda, it must be bound to a functional interface. I’m using the predefined Consumer
functional interface, which meets the method reference/lambda requirements. The sort operation commences by passing the array to be sorted to Consumer
‘s accept()
method.
Compile Listing 1 (javac MRDemo.java
) and run the application (java MRDemo
). You’ll observe the following output:
2
5
10
17
19
3
4
5
14
19
21
References to bound non-static methods
A bound non-static method reference refers to a non-static method that’s bound to a receiver object. Its syntax is objectName::instanceMethodName
, where objectName
identifies the receiver and instanceMethodName
identifies the instance method. An example is s::trim
. Listing 2 demonstrates a bound non-static method reference.
Listing 2. MRDemo.java (version 2)
import java.util.function.Supplier;
public class MRDemo
{
public static void main(String[] args)
{
String s = "The quick brown fox jumped over the lazy dog";
print(s::length);
print(() -> s.length());
print(new Supplier<Integer>()
{
@Override
public Integer get()
{
return s.length(); // closes over s
}
});
}
public static void print(Supplier<Integer> supplier)
{
System.out.println(supplier.get());
}
}
Listing 2’s main()
method assigns a string to String
variable s
and then invokes the print()
class method with functionality to obtain this string’s length as this method’s argument. print()
is invoked in method reference (s::length
— length()
is bound to s
), equivalent lambda, and equivalent anonymous class contexts.
I’ve defined print()
to use the java.util.function.Supplier
predefined functional interface, whose get()
method returns a supplier of results. In this case, the Supplier
instance passed to print()
implements its get()
method to return s.length()
; print()
outputs this length.
s::length
introduces a closure that closes over s
. You can see this more clearly in the lambda example. Because the lambda has no arguments, the value of s
is only available from the enclosing scope. Therefore, the lambda body is a closure that closes over s
. The anonymous class example makes this even clearer.
Compile Listing 2 and run the application. You’ll observe the following output:
44
44
44
References to unbound non-static methods
An unbound non-static method reference refers to a non-static method that’s not bound to a receiver object. Its syntax is className::instanceMethodName
, where className
identifies the class that declares the instance method and instanceMethodName
identifies the instance method. An example is String::toLowerCase
.
String::toLowerCase
is an unbound non-static method reference that identifies the non-static String toLowerCase()
method of the String
class. However, because a non-static method still requires a receiver object (in this example a String
object, which is used to invoke toLowerCase()
via the method reference), the receiver object is created by the virtual machine. toLowerCase()
will be invoked on this object. String::toLowerCase
specifies a method that takes a single String
argument, which is the receiver object, and returns a String
result. String::toLowerCase()
is equivalent to lambda (String s) -> { return s.toLowerCase(); }
.
Listing 3 demonstrates this unbound non-static method reference.
Listing 3. MRDemo.java (version 3)
import java.util.function.Function;
public class MRDemo
{
public static void main(String[] args)
{
print(String::toLowerCase, "STRING TO LOWERCASE");
print(s -> s.toLowerCase(), "STRING TO LOWERCASE");
print(new Function<String, String>()
{
@Override
public String apply(String s) // receives argument in parameter s;
{ // doesn't need to close over s
return s.toLowerCase();
}
}, "STRING TO LOWERCASE");
}
public static void print(Function<String, String> function, String s)
{
System.out.println(function.apply(s));
}
}
Listing 3’s main()
method invokes the print()
class method with functionality to convert a string to lowercase and the string to be converted as the method’s arguments. print()
is invoked in method reference (String::toLowerCase
, where toLowerCase()
isn’t bound to a user-specified object) and equivalent lambda and anonymous class contexts.
I’ve defined print()
to use the java.util.function.Function
predefined functional interface, which represents a function that accepts one argument and produces a result. In this case, the Function
instance passed to print()
implements its R apply(T t)
method to return s.toLowerCase()
; print()
outputs this string.
Although the String
part of String::toLowerCase
makes it look like a class is being referenced, only an instance of this class is referenced. The anonymous class example makes this more obvious. Note that in the anonymous class example the lambda receives an argument; it doesn’t close over parameter s
(i.e., it’s not a closure).
Compile Listing 3 and run the application. You’ll observe the following output:
string to lowercase
string to lowercase
string to lowercase
References to constructors
You can use a method reference to refer to a constructor without instantiating the named class. This kind of method reference is known as a constructor reference. Its syntax is className::new
. className
must support object creation; it cannot name an abstract class or interface. Keyword new
names the referenced constructor. Here are some examples:
Character::new
: equivalent to lambda(Character ch) -> new Character(ch)
Long::new
: equivalent to lambda(long value) -> new Long(value)
or(String s) -> new Long(s)
ArrayList<City>::new
: equivalent to lambda() -> new ArrayList<City>()
float[]::new
: equivalent to lambda(int size) -> new float[size]
The last constructor reference example specifies an array type instead of a class type, but the principle is the same. The example demonstrates an array constructor reference to the “constructor” of an array type.
To create a constructor reference, specify new
without a constructor. When a class such as java.lang.Long
declares multiple constructors, the compiler compares the functional interface’s type against all of the constructors and chooses the best match. Listing 4 demonstrates a constructor reference.
Listing 4. MRDemo.java (version 4)
import java.util.function.Supplier;
public class MRDemo
{
public static void main(String[] args)
{
Supplier<MRDemo> supplier = MRDemo::new;
System.out.println(supplier.get());
}
}
Listing 4’s MRDemo::new
constructor reference is equivalent to lambda () -> new MRDemo()
. Expression supplier.get()
executes this lambda, which invokes MRDemo
‘s default no-argument constructor and returns the MRDemo
object, which is passed to System.out.println()
. This method converts the object to a string, which it prints.
Now suppose you have a class with a no-argument constructor and a constructor that takes an argument, and you want to call the constructor that takes an argument. You can accomplish this task by choosing a different functional interface, such as the predefined Function
interface shown in Listing 5.
Listing 5. MRDemo.java (version 5)
import java.util.function.Function;
public class MRDemo
{
private String name;
MRDemo()
{
name = "";
}
MRDemo(String name)
{
this.name = name;
System.out.printf("MRDemo(String name) called with %s%n", name);
}
public static void main(String[] args)
{
Function<String, MRDemo> function = MRDemo::new;
System.out.println(function.apply("some name"));
}
}
Function<String, MRDemo> function = MRDemo::new;
causes the compiler to look for a constructor that takes a String
argument, because Function
‘s apply()
method requires a single (in this context) String
argument. Executing function.apply("some name")
results in "some name"
being passed to MRDemo(String name)
.
Compile Listing 5 and run the application. You’ll observe output that’s similar to the following:
MRDemo(String name) called with some name
MRDemo@2f92e0f4
References to instance methods in superclass and current class types
Java’s super
keyword, which can be used only in instance contexts, references an overridden method in a superclass. You may occasionally need to create a method reference that refers to a superclass method. You can do so via the following syntax:
className.super::instanceMethod
Java’s this
keyword, which can be used only in instance contexts, invokes a constructor or references an instance method in the current class. You may occasionally need to create a method reference that refers to a method in the current class. You can do so via the following syntax:
this::instanceMethod
Listing 6 presents an application that demonstrates both syntaxes.
Listing 6. MRDemo.java (version 6)
import java.util.function.Consumer;
class Superclass
{
void print(String msg)
{
System.out.printf("Superclass print(): %s%n", msg);
}
}
class Subclass extends Superclass
{
Subclass()
{
Consumer<String> consumer = Subclass.super::print;
consumer.accept("Subclass.super::print");
consumer = this::print;
consumer.accept("this::print");
}
@Override
void print(String msg)
{
System.out.printf("Subclass print(): %s%n", msg);
}
}
public class MRDemo
{
public static void main(String[] args)
{
new Subclass();
}
}
Listing 6 introduces the Superclass
and Subclass
classes, where Subclass
extends Superclass
. Subclass
introduces a constructor and overrides Superclass
‘s void print(String msg)
method. I’ve also introduced an MRDemo
class whose main()
method drives the application by instantiating Subclass
.
Subclass
‘s constructor assigns a method reference to Superclass
‘s print()
method to a Consumer<String>
variable (void print(String)
matches void
accept(String)
‘s return type and parameter list), and invokes the method. Next, the constructor assigns a method reference to Subclass
‘s print()
method to this variable and invokes the method.
Compile Listing 6 and run the application. You’ll observe the following output:
Superclass print(): Subclass.super::print
Subclass print(): this::print
In conclusion
Lambdas and method references help us express functionality in a compact manner, which makes source code easier to understand. In this tutorial, I’ve shown you how to use method references to refer to a class’s static methods, bound and unbound non-static methods, and constructors, as well as how to use them to refer to instance methods in superclass and current class types. Keep practicing with lambdas and method references, and get in the habit of using these features instead of the more verbose anonymous classes.