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));
}
}
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;
}
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"
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"
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}
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"
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);
Real-World Patterns
-
Map Aggregation:
Map.merge,Map.compute, andMap.computeIfPresentall take aBiFunctionto 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
BiFunctioninstead of a fixed method.
Best Practices
-
Name for Both Inputs and the Result:
toReceipt,mergeCounts,describesay more thanapply. -
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
BiFunctionthat 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)