This tutorial shows you how to pass a method as a parameter of another method in Java using a functional interface.
When writing code, sometimes you may have some methods that do a similar thing, except there is a little difference in one part. In that case, it would be better to only write the similar logic once. There are several ways to achieve that. For example, you can apply a design pattern, such as by defining an interface or an abstract class with a method to be overridden by each implementing class. However, if you don't want to have a separate class for each implementation, there is another simple solution. In Java, you can pass a method as an argument of another method.
Pass a Method with Parameters a Return Value
In this section, I am going to explain how to define a parameter that accepts a method with a return value. This includes how to run the method in order to get the returned value.
One Parameter with a Return Value (Using Function
)
For example, we have a method named generateData
which is used to generate a list of numbers. It performs argument validation, data generation, as well as result ordering and mapping. For the data generation part, we want to let it be handled by another method passed as an argument. That means generateData
needs to have a parameter that accepts a method.
Our focus here is how to define a parameter which allows a method to be passed. In addition, we also want the passed method to have a parameter whose type is an integer and it has to return a List<Integer>
. In other words, we want to make sure that the parameters and the return type must be in accordance with a specification.
Since Java 1.8, you can use a FunctionalInterface
for that purpose. A functional interface has exactly one abstract method. If you ever look at the source code of Java, actually there are a lot of methods using a functional interface as its parameter. For example, Stream
's forEach
uses a Consumer
, while Stream
's filter
uses a Predicate
.
For a method with one parameter and a non-void return type, Java already provides a functional interface called Function
in the java.util.function
package.
Function<T, R>
The Function
interface has two parameterized types T
and R
. T
is the type of the parameter, while R
is the return type. So, to define a parameter whose type is a method with an integer parameter and List<Integer>
return type, you can write Function<Integer, List<Integer>>
. Then, you can call the apply
method of the interface to call the passed method with an argument whose type is T
. After the execution finishes, it will return a value whose type is R
. Below is the generateData
method which has a parameter that accepts a Function
.
public GenerateDataResult generateData(int n, Function<Integer, List<Integer>> dataGenerator) {
if (n < 0 || n > 100) {
throw new IllegalArgumentException();
}
List<Integer> data = dataGenerator.apply(n)
.stream()
.sorted()
.collect(Collectors.toList());
return GenerateDataResult.builder()
.generatedAt(ZonedDateTime.now())
.data(data)
.build();
}
Next, create a method to be passed as the dataGenerator
argument.
public List<Integer> generateRandomNumbers(int n) {
final Random random = new Random();
final List<Integer> result = new ArrayList<>();
for (int i = 0; i < n; i++) {
result.add(random.nextInt());
}
return result;
}
To pass the method, you can use a lambda. It can also be done using a method reference if you use Java 8 or above.
// lambda
GenerateDataResult result = this.generateData(10, n -> this.generateRandomNumbers(n));
// method reference
GenerateDataResult result = this.generateData(10, this::generateRandomNumbers);
Two Parameters with a Return Value (Using BiFunction
)
Let's say there is an additional parameter that should be added to the dataGenerator
method. It should have a second parameter whose type is a boolean that indicates whether it's allowed to return a negative number. Because it has two parameters, we cannot use the Function
interface. However, Java already provides another functional interface called BiFunction
BiFunction<T, U, R>
BiFunction
has three parameterized types T
, U
, and R
. The first two are the types of the parameters in order, while the last one is the return type. Below is the example of how to define the parameter whose type is BiConsumer
. Since we expect dataGenerator
to have two parameters whose types in order are integer and boolean, with List<Integer>>
as the return type, we can define the type as BiFunction<Integer, Boolean, List<Integer>>
.
public GenerateDataResult generateData(int n, boolean allowNegative, BiFunction<Integer, Boolean, List<Integer>> dataGenerator) {
if (n < 0 || n > 100) {
throw new IllegalArgumentException();
}
List<Integer> data = dataGenerator.apply(n, allowNegative)
.stream()
.sorted()
.collect(Collectors.toList());
return GenerateDataResult.builder()
.generatedAt(ZonedDateTime.now())
.data(data)
.build();
}
Below is the updated generateRandomNumbers
method which has two parameters.
public List<Integer> generateRandomNumbers(int n, boolean allowNegative) {
final Random random = new Random();
final List<Integer> result = new ArrayList<>();
for (int i = 0; i < n; i++) {
int value = random.nextInt();
boolean shouldNegate = allowNegative && random.nextBoolean();
result.add(shouldNegate ? -value : value);
}
return result;
}
The rest is the same, you can use a lambda or a method reference to pass the generateRandomNumbers
method as the parameter of generateData
.
// lambda
GenerateDataResult result = this.generateData(10, true, (n, allowNegative) -> this.generateRandomNumbers(n, allowNegative));
// method reference
GenerateDataResult result = this.generateData(10, true, this::generateRandomNumbers);
Three or More Parameters with a Return Value (Using Custom Functional Interface)
If the passed method has more than two parameters, you may need to create your own functional interface since Java only provides functional interfaces with up to two parameters at this moment.
For example, below is a custom functional interface called TriFunction
which is suitable for a method with three parameters and a return value.
import java.util.Objects;
import java.util.function.Function;
@FunctionalInterface
public interface TriFunction<T, U, V, R> {
R apply(T t, U u, V v);
default <W> TriFunction<T, U, V, W> andThen(Function<? super R, ? extends W> after) {
Objects.requireNonNull(after);
return (T t, U u, V v) -> after.apply(apply(t, u, v));
}
}
Let's change the requirement for the dataGenerator
method to have a third parameter whose type is integer which defines the upper bound of the generated values. We can use the TriFunction
interface to define the parameter type as TriFunction<Integer, Boolean, Integer, List<Integer>> dataGenerator
.
Below is the updated generateData
method where the parameter of the data generator is defined using the TriFunction
.
public GenerateDataResult generateData(int n, boolean allowNegative, int upperBound, TriFunction<Integer, Boolean, Integer, List<Integer>> dataGenerator) {
if (n < 0 || n > 100) {
throw new IllegalArgumentException();
}
List<Integer> data = dataGenerator.apply(n, allowNegative, upperBound)
.stream()
.sorted()
.collect(Collectors.toList());
return GenerateDataResult.builder()
.generatedAt(ZonedDateTime.now())
.data(data)
.build();
}
We also update the generateRandomNumbers
method to handle the additional parameter.
public List<Integer> generateRandomNumbers(int n, boolean allowNegative, int upperBound) {
final Random random = new Random();
final List<Integer> result = new ArrayList<>();
for (int i = 0; i < n; i++) {
int value = random.nextInt(upperBound);
boolean shouldNegate = allowNegative && random.nextBoolean();
result.add(shouldNegate ? -value : value);
}
return result;
}
Then, you can use a lambda or a method reference to pass it as an argument.
// lambda
GenerateDataResult result = this.generateData(10, true, 100, (n, allowNegative, upperBound) -> this.generateRandomNumbers(n, allowNegative, upperBound));
// method reference
GenerateDataResult result = this.generateData(10, true, 100, this::generateRandomNumbers);
Pass a Method with Parameters without a Return Value
In Java, you can also define methods without any return value (void
). For example, there is a method named printData
which uses another passed method to handle printing the data. The passed method does't have any return value. If you want to have it as a parameter, you should not use the Function
or BiFunction
since the method doesn't have any return value. However, Java has some functional interfaces that's suitable for operations without a return value.
One Parameter without a Return Value (Using Consumer
)
If the passed method only has one parameter, the solution is to use a functional interface called Consumer
, which represents an operation that accepts a single input argument and returns no result.
The Consumer
interface has a parameterized type T
which represents the type of parameter.
interface Consumer<T>
For example, if the type is integer, you can write it as Consumer<Integer>
.
Below is an example of how to define a parameter whose type is a Consumer
.
public void printData(int n, Consumer<Integer> dataWriter) {
if (n < 0 || n > 100) {
throw new IllegalArgumentException();
}
dataWriter.accept(n);
}
And below is the method that's compatible with that type.
public void printRandomNumbers(int n) {
final Random random = new Random();
for (int i = 0; i < n; i++) {
System.out.println(random.nextInt());
}
}
Then, you can pass it using a lambda or a method reference.
// lambda
this.printData(10, n -> this.printRandomNumbers(n));
// method reference
this.printData(10, this::printRandomNumbers);
Two Parameters without a Return Value (Using BiConsumer
)
If the method to be passed has two parameters and doesn't have a return value, you can use the BiConsumer
functional interface.
interface BiConsumer<T, U>
For example, we want to change that the passed dataWriter
must have an additional argument whose type is a boolean. We can define the type as BiConsumer<Integer, Boolean>
.
public void printData(int n, boolean allowNegative, BiConsumer<Integer, Boolean> dataWriter) {
if (n < 0 || n > 100) {
throw new IllegalArgumentException();
}
dataWriter.accept(n, allowNegative);
}
Below is the method to be passed.
public void printRandomNumbers(int n, boolean allowNegative) {
final Random random = new Random();
for (int i = 0; i < n; i++) {
int value = random.nextInt();
boolean shouldNegate = allowNegative && random.nextBoolean();
System.out.println(shouldNegate ? -value : value);
}
}
And these are the examples of how to pass it.
// lambda
this.printData(10, true, (n, allowNegative) -> this.printRandomNumbers(n, allowNegative));
// method reference
this.printData(10, true, this::printRandomNumbers);
Three or More Parameters without a Return Value (Using Custom Functional Interface)
If the method to be passed has more than two parameters, there is no built-in functional interface from Java. Therefore, you have to create your own. Below is a functional interface that defines an operation with three parameters and no return type.
@FunctionalInterface
public interface TriConsumer<T, U, V> {
void accept(T t, U u, V v);
default TriConsumer<T, U, V> andThen(TriConsumer<? super T, ? super U, ? super V> after) {
Objects.requireNonNull(after);
return (t, u, v) -> {
accept(t, u, v);
after.accept(t, u, v);
};
}
}
For example, we change the requirement of the passed dataWriter
method to have a third parameter which is as integer to be set as the upper bound, we can define the type using the custom functional interface above as TriConsumer<Integer, Boolean, Integer>
.
public void printData(
int n,
boolean allowNegative,
int upperBound,
TriConsumer<Integer, Boolean, Integer> dataWriter
) {
if (n < 0 || n > 100) {
throw new IllegalArgumentException();
}
dataWriter.accept(n, allowNegative, upperBound);
}
Below is the adjusted printRandomNumbers
method that has three parameters.
public void printRandomNumbers(int n, boolean allowNegative, int upperBound) {
final Random random = new Random();
for (int i = 0; i < n; i++) {
int value = random.nextInt(upperBound);
boolean shouldNegate = allowNegative && random.nextBoolean();
System.out.println(shouldNegate ? -value : value);
}
}
You can pass the printRandomNumbers
using a lambda or a method reference.
// lambda
this.printData(
10,
true,
100,
(n, allowNegative, upperBound) -> this.printRandomNumbers(n, allowNegative, upperBound));
// method reference
this.printData(10, true, 100, this::printRandomNumbers);
Pass a Method without Parameter with a Return Value (Using Supplier
)
We have another case where the passed method doesn't have any parameter but has a return value. Java has a functional interface called Supplier
that's suitable for this case.
interface Supplier<T>
For example, to define a method without any parameter whose return type is List<Integer>
. It can be defined as Supplier<List<Integer>>
.
public void generateData(Supplier<List<Integer>> dataGenerator) {
List<Integer> numbers = dataGenerator.get();
for (Integer number : numbers) {
System.out.println(number);
}
}
Below is the Supplier
method that can be passed as the dataGenerator
.
public List<Integer> generateRandomNumbers() {
final Random random = new Random();
final List<Integer> result = new ArrayList<>();
for (int i = 0; i < 10; i++) {
result.add(random.nextInt());
}
return result;
}
And these are the examples of how to pass it.
// lambda
this.generateData(() -> this.generateRandomNumbers());
// method reference
this.generateData(this::generateRandomNumbers);
Actually there are still a lot of built-in functional interfaces that's not used in this tutorial such as Predicate
, BiPredicate
, IntFunction
, etc. You can see the list in the java.util.function
package.
Summary
That's how to pass a method as a parameter of another method. You can use a functional interface as the parameter type and it must be compatible with the methods that can be passed. Java provides a lot of functional interfaces in the java.util.function
package. If none of them is suitable with what you need, you have to create your own functional interface. Then, you can pass the method using a lambda or a method reference.