Definition
- **BiConsumer<T,U>** – `(T, U) -> void`; used by `Map.forEach(key, value -> action)`. - **BiFunction<T,U,R>** – `(T, U) -> R`; used by `Map.merge`, `Map.compute`, `Map.replaceAll`. - **BiPredicate<T,U>** – `(T, U) -> boolean`; used for two-input conditions, composable with `and()`, `or()`, `negate()`. - **Relationship** – "Bi" adds one more parameter: Consumer(T->void) becomes BiConsumer(T,U->void). - **No TriFunction** – for three or more arguments, define a custom `@FunctionalInterface`.
Consumer takes one thing and acts on it (process one cheque). BiConsumer takes two things (process a key-value pair: account number and balance). Map.forEach gives you both the key and the value, so you need BiConsumer to accept both.
Understanding the Concepts
What are BiConsumer, BiPredicate, BiFunction?
BiConsumer: two inputs, no return
import java.math.BigDecimal;
import java.util.Map;
import java.util.function.BiConsumer;
Map<String, BigDecimal> balances = Map.of(
"SAV2001", new BigDecimal("125000"),
"CUR3001", new BigDecimal("850000"));
// Map.forEach takes BiConsumer<K, V>
balances.forEach((accountId, balance) -> {
System.out.println("Account " + accountId + ": Rs." + balance);
});
// Composition: andThen chains two BiConsumers
BiConsumer<String, BigDecimal> logIt = (id, bal) -> System.out.println(id + ": " + bal);
BiConsumer<String, BigDecimal> auditIt = (id, bal) -> auditService.record(id, bal);
balances.forEach(logIt.andThen(auditIt));
BiFunction: two inputs, one return
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
// Map.merge: BiFunction resolves key collision
Map<String, BigDecimal> totals = new HashMap<>();
trades.forEach(t -> {
totals.merge(t.getSymbol(), t.getAmount(), BigDecimal::add);
});
// merge(key, newValue, BiFunction<existing, newValue, result>)
// Map.compute: BiFunction computes from key + existing value
balances.compute("SAV2001", (key, existing) -> {
return existing == null ? BigDecimal.ZERO : existing.add(interest);
});
// Map.replaceAll: BiFunction transforms all values
balances.replaceAll((accountId, balance) ->
balance.multiply(new BigDecimal("1.05"))); // 5% interest
BiPredicate: two inputs, boolean return
import java.math.BigDecimal;
import java.util.function.BiPredicate;
BiPredicate<Trade, BigDecimal> exceedsLimit = (trade, limit) ->
trade.getAmount().compareTo(limit) > 0;
// Composition:
BiPredicate<Trade, BigDecimal> exceedsAndSettled = exceedsLimit
.and((trade, limit) -> trade.isSettled());
boolean result = exceedsLimit.test(trade, new BigDecimal("100000"));
What is the relationship to single-argument counterparts?
| Single | Bi | Difference |
|---|---|---|
| Consumer
| Function
| Predicate
What should a senior engineer know?
- Map.merge with BigDecimal::add is the idiomatic way to aggregate by key in one line.
- Map.compute returns null to remove the entry, or a new value to update it.
- For three arguments, define a custom interface:
@FunctionalInterface interface TriFunction { R apply(A a, B b, C c); }.
What are common interview follow-up questions?
- What Map methods use BiFunction? (merge, compute, computeIfPresent, replaceAll.)
- Can BiConsumer return a value? (No. Use BiFunction for that.)
- Is there a TriConsumer? (No. Define your own or use currying.)
What are common mistakes?
// WRONG: using Consumer when two inputs are needed
// Map.forEach needs BiConsumer, not Consumer
// balances.forEach(id -> System.out.println(id)); // COMPILE ERROR: expects (K, V)
// CORRECT: BiConsumer accepts two inputs
balances.forEach((id, balance) -> System.out.println(id + ": Rs." + balance));
Visual Overview
Bi-variant Relationships: Consumer<T> ──+1 param──▶ BiConsumer<T, U> T -> void (T, U) -> void list.forEach Map.forEach Function<T, R> ──+1 param──▶ BiFunction<T, U, R> T -> R (T, U) -> R stream.map Map.merge, Map.compute Predicate<T> ──+1 param──▶ BiPredicate<T, U> T -> boolean (T, U) -> boolean stream.filter two-input conditions
Key Takeaway
BiConsumer, BiPredicate, and BiFunction are two-argument versions of the core functional interfaces. BiConsumer takes two inputs and returns void, used by Map.forEach. BiFunction takes two inputs and returns a result, used by Map.merge, Map.compute, and Map.replaceAll. BiPredicate takes two inputs and returns boolean.
The relationship is simple: Bi adds one more parameter. Consumer becomes BiConsumer, Function becomes BiFunction, Predicate becomes BiPredicate. For three or more arguments, there are no built-in variants; define a custom @FunctionalInterface.
In banking, Map.merge("RELIANCE", amount, BigDecimal::add) uses BiFunction to sum duplicate trades. Map.forEach((accountId, balance) -> log) uses BiConsumer. Map.replaceAll((id, bal) -> bal.multiply(rate)) applies interest to all balances.
Remember one sentence: BiConsumer, BiPredicate, and BiFunction are the two-argument versions of Consumer, Predicate, and Function, used primarily with Map operations.
---