Java 8 BiFunction, BiConsumer and BiPredicate Tutorial

Introduction

This tutorial explains the three two-argument functional interfaces in Java 8's java.util.function package: BiFunction, BiConsumer and BiPredicate. We will start with how they relate to the one-argument interfaces you probably know - Function, Consumer and Predicate. We will then look at each one's definition and default methods, run one example that uses all three (including method references and a constructor reference), and finish with where the JDK itself uses them and what to do when you need three arguments.

(If you are new to functional interfaces, start with the java.util.function package tutorialClick to read: java.util.function package tutorial and the FunctionClick to read: Function, ConsumerClick to read: Consumer and PredicateClick to read: Predicate tutorials.)

The one-argument interfaces and their Bi- twins

Function, Consumer and Predicate each take one argument. Many real operations need two: a salary and a raise percentage, a map key and its value, an employee and a threshold. For these, the package has a Bi- version of each interface that takes two arguments of possibly different types, T and U.
Java 8 Function, Consumer and Predicate and their two-argument versions BiFunction, BiConsumer and BiPredicate

BiFunction<T, U, R>

Definition of BiFunction in java.util.function is -
R apply(T t, U u) AND default <V> BiFunction<T,U,V> andThen(Function<? super R,? extends V> after)
Where,
  • apply() takes a T and a U and returns an R. Its function descriptorClick to read: function descriptor is (T, U) -> R.
  • andThen() returns a new BiFunction that passes the result to a one-argument Function. There is no compose(), because a function that runs before would have to produce two values.

BiConsumer<T, U>

Definition of BiConsumer in java.util.function is -
void accept(T t, U u) AND default BiConsumer<T,U> andThen(BiConsumer<? super T,? super U> after)
Where,
  • accept() takes two arguments and returns nothing; it exists for its side effect. Descriptor: (T, U) -> void.
  • andThen() returns a BiConsumer that runs this one and then after, both with the same two arguments.

BiPredicate<T, U>

Definition of BiPredicate in java.util.function is -
boolean test(T t, U u) AND default BiPredicate<T,U> and(...), or(...), negate()
Where,
  • test() checks a condition on two arguments. Descriptor: (T, U) -> boolean.
  • and(), or() and negate() combine BiPredicates exactly like their Predicate counterparts; and() and or() short-circuit.
Combining BiFunction with andThen, BiConsumer with andThen and BiPredicate with and, or and negate in Java 8

Java 8 code example of BiFunction, BiConsumer and BiPredicate

The example uses a small Employee class with a name and a monthly salary.
Employee.java
//Employee.java
public class Employee {
  private final String name;
  private final double salary;

  public Employee(String name, double salary) {
    this.name = name;
    this.salary = salary;
  }

  public String getName() { return name; }
  public double getSalary() { return salary; }

  @Override
  public String toString() { return name + " (" + salary + ")"; }
}
Java 8 code showing BiFunction, BiConsumer and BiPredicate with andThen, and, negate and method references
//BiInterfaces.java
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Function;

public class BiInterfaces {

  public static void main(String[] args) {
    // 1. BiFunction<T, U, R>: two inputs, one result
    BiFunction<Employee, Double, Double> raised =
        (e, percent) -> e.getSalary() * (1 + percent / 100);
    Employee tom = new Employee("Tom Jones", 5000);
    System.out.println("1. Tom after a 4% raise: " + raised.apply(tom, 4.0));

    // 2. andThen(Function): pass the BiFunction's result to a Function
    Function<Double, String> asText = amount -> String.format("%.2f a month", amount);
    System.out.println("2. " + raised.andThen(asText).apply(tom, 4.0));

    // 3. A constructor reference as a BiFunction
    BiFunction<String, Double, Employee> hire = Employee::new;
    Employee nancy = hire.apply("Nancy Smith", 5800.0);
    System.out.println("3. hired: " + nancy);

    // 4. BiConsumer<T, U>: two inputs, no result - Map.forEach takes one
    Map<String, Integer> leaveLeft = new TreeMap<>();
    leaveLeft.put("Tom Jones", 12);
    leaveLeft.put("Harry Major", 3);
    BiConsumer<String, Integer> show =
        (name, days) -> System.out.print(name + ": " + days + "  ");
    BiConsumer<String, Integer> warn = (name, days) -> {
      if (days < 5) System.out.print("(low!)  ");
    };
    System.out.print("4. ");
    leaveLeft.forEach(show.andThen(warn));
    System.out.println();

    // 5. BiPredicate<T, U>: a test on two inputs
    BiPredicate<Employee, Double> earnsAtLeast = (e, amount) -> e.getSalary() >= amount;
    BiPredicate<Employee, Double> underDouble = (e, amount) -> e.getSalary() <= amount * 2;
    BiPredicate<Employee, Double> inBand = earnsAtLeast.and(underDouble);
    System.out.println("5. Nancy at least 5000? " + earnsAtLeast.test(nancy, 5000.0)
        + ", in band 3000-6000? " + inBand.test(nancy, 3000.0)
        + ", not at least 6000? " + earnsAtLeast.negate().test(nancy, 6000.0));

    // 6. Method references that match BiPredicate and BiFunction
    BiPredicate<String, String> startsWith = String::startsWith;
    BiFunction<Integer, Integer, Integer> larger = Math::max;
    System.out.println("6. \"Harry Major\" starts with \"Har\"? "
        + startsWith.test("Harry Major", "Har")
        + ", larger of 7 and 12: " + larger.apply(7, 12));

    // 7. Passing a BiFunction into a method
    List<Employee> team = Arrays.asList(tom, nancy, new Employee("Harry Major", 4100));
    double bonuses = payAll(team, 10.0, (e, pct) -> e.getSalary() * pct / 100);
    System.out.println("7. total bonuses at 10%: " + bonuses);
  }

  // Applies any two-argument rule to every employee
  static double payAll(List<Employee> team, double rate,
                       BiFunction<Employee, Double, Double> rule) {
    double total = 0;
    for (Employee e : team) {
      total += rule.apply(e, rate);
    }
    return total;
  }
}
 OUTPUT of the above code
1. Tom after a 4% raise: 5200.0
2. 5200.00 a month
3. hired: Nancy Smith (5800.0)
4. Harry Major: 3  (low!)  Tom Jones: 12
5. Nancy at least 5000? true, in band 3000-6000? true, not at least 6000? true
6. "Harry Major" starts with "Har"? true, larger of 7 and 12: 12
7. total bonuses at 10%: 1490.0

Explanation of the code

  • Part 1: raised takes an Employee and a percentage and returns the new salary. The types of its two inputs differ, which is exactly what BiFunction<T, U, R> allows.
  • Part 2: andThen(asText) builds a new BiFunction that formats the raised salary as text.
  • Part 3: the two-argument constructor Employee(String, double) matches BiFunction<String, Double, Employee>, so Employee::new can be used as one - see constructor referencesClick to read: constructor references.
  • Part 4: Map.forEach() takes a BiConsumer of key and value. show.andThen(warn) prints each entry and then a warning for low balances.
  • Part 5: earnsAtLeast.and(underDouble) tests both conditions with the same employee and amount, and negate() inverts a test.
  • Part 6: method referencesClick to read: method references fit the Bi- interfaces too. String::startsWith is an instance method called on the first argument with the second as its parameter, so it is a BiPredicate<String, String>; Math::max is a static method with two parameters.
  • Part 7: payAll() accepts any two-argument rule, so the bonus calculation is passed in as a lambda - the behaviour is a parameter, like the strategies in the Strategy patternClick to read: Strategy pattern.

Where the JDK uses them

What about three arguments?

Java 8 stops at two: there is no TriFunction. If you need more inputs, either group them into one object - often the clearer design - or declare your own interface, annotated with @FunctionalInterface -
A custom three-argument functional interface
@FunctionalInterface
interface TriFunction<A, B, C, R> {
  R apply(A a, B b, C c);
}

TriFunction<Double, Double, Integer, Double> compound =
    (amount, rate, years) -> amount * Math.pow(1 + rate, years);

Summary

In this tutorial we first saw that BiFunction, BiConsumer and BiPredicate are the two-argument versions of Function, Consumer and Predicate. Next we looked at each interface's abstract method, function descriptor and default methods: andThen() for BiFunction and BiConsumer, and and(), or() and negate() for BiPredicate. We then ran an example covering all three, including a constructor reference and method references that match them. Lastly, we saw where the JDK uses these interfaces and how to handle three or more arguments.