DEV Community

Cover image for Surfing with FP Java - Mastering BiFunction<T, U, R>
André Borba
André Borba

Posted on

Surfing with FP Java - Mastering BiFunction<T, U, R>

Introduction

In the last episode, BinaryOperator combined two values of the same type into one.

Now, let's generalize that idea with BiFunction: two inputs, possibly of different types, transformed into a result of yet another type.

If Function transforms one value, BiFunction transforms a pair.

What Is BiFunction?

@FunctionalInterface
public interface BiFunction<T, U, R> {

    R apply(T t, U u);

    default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t, U u) -> after.apply(apply(t, u));
    }
}
Enter fullscreen mode Exit fullscreen mode

Input: two objects, of types T and U.
Output: one object of type R.
Purpose: transform a pair of values, possibly of different types, into a single result.

Why Use BiFunction?

Before, combining two independent pieces of data meant a bespoke method for every combination:

static String label(String name, int score) {
    return name + ": " + score;
}
Enter fullscreen mode Exit fullscreen mode

BiFunction turns that combination logic into a value you can pass around:

BiFunction<String, Integer, String> label = (name, score) -> name + ": " + score;

System.out.println(label.apply("Borba", 95)); // "Borba: 95"
Enter fullscreen mode Exit fullscreen mode

Practical Examples

1. Combining Two Inputs into a Result

BiFunction<Integer, Integer, String> describe = (a, b) -> a + " + " + b + " = " + (a + b);

System.out.println(describe.apply(2, 3)); // "2 + 3 = 5"
Enter fullscreen mode Exit fullscreen mode

2. Map.merge and Map.compute

Map<String, Integer> stock = new HashMap<>();
stock.put("apples", 10);

BiFunction<Integer, Integer, Integer> addStock = Integer::sum;

stock.merge("apples", 5, addStock);
// stock: {apples=15}
Enter fullscreen mode Exit fullscreen mode

3. Chaining with andThen

BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b;

BiFunction<Integer, Integer, String> multiplyThenDescribe =
    multiply.andThen(result -> "Result: " + result);

System.out.println(multiplyThenDescribe.apply(6, 7)); // "Result: 42"
Enter fullscreen mode Exit fullscreen mode

4. Building a Value Object from Two Sources

BiFunction<User, Order, Receipt> toReceipt = (user, order) ->
    new Receipt(user.getName(), order.getTotal());

Receipt receipt = toReceipt.apply(currentUser, currentOrder);
Enter fullscreen mode Exit fullscreen mode

Real-World Patterns

  • Map Aggregation: Map.merge, Map.compute, and Map.computeIfPresent all take a BiFunction to combine or update values.
  • DTO / Record Assembly: Combining two independent objects, a user and an order, a request and a context, into one composed result.
  • Custom Reducers with Heterogeneous Types: Folding a stream of raw input into an accumulator of a different type.
  • Service Layer Methods: Many two-argument business operations can be expressed as a stored, reusable BiFunction instead of a fixed method.

Best Practices

  • Name for Both Inputs and the Result: toReceipt, mergeCounts, describe say more than apply.
  • Keep It Pure: Avoid mutating either input inside apply, return a new result instead.
  • Use andThen for Post-Processing: Instead of writing the follow-up transformation inline, compose it.

Common Pitfalls

  • Reaching for Three or More Arguments: The JDK stops at two, wrapping extra parameters into a BiFunction<T, U, R> via a map or array is a sign a small record or a dedicated interface would be clearer.
  • Generic Overload: Long chains of type parameters hurt readability, consider a named type instead.
  • Mixing in Side Effects: A BiFunction that both computes a result and logs, saves, or mutates blurs its contract.

Functional Analogy

Think of BiFunction as a chef:

  • Two ingredients come in, T and U.
  • The chef combines them.
  • A single dish comes out, R.

Conclusion

BiFunction generalizes two-argument transformation, essential wherever a result depends on combining two independent pieces of information. It's the two-argument counterpart to everything Function already taught us.

What's Next

In the next episode, we return to boolean logic with BiPredicate, testing two values together to decide true or false.

Top comments (0)