Java - Pass a Method As a Parameter Examples

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.