DEV Community

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

Posted on

Surfing with FP Java - Mastering BiConsumer<T, U>

Introduction

In the last episode, BiPredicate evaluated two values together and returned a verdict.

Now, in our final stop, let's meet BiConsumer: it takes two inputs and performs an action, no result, just effect.

If Consumer acts on one value, BiConsumer acts on a pair.

What Is BiConsumer?

@FunctionalInterface
public interface BiConsumer<T, U> {

    void accept(T t, U u);

    default BiConsumer<T, U> andThen(BiConsumer<? super T, ? super U> after) {
        Objects.requireNonNull(after);
        return (t, u) -> {
            accept(t, u);
            after.accept(t, u);
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Input: two objects, of types T and U.
Output: none (void).
Purpose: perform an action that depends on two values at once, typically with side effects.

Why Use BiConsumer?

Before, iterating a map and doing something with both key and value meant a hand-rolled loop:

for (Map.Entry<String, Integer> entry : stock.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}
Enter fullscreen mode Exit fullscreen mode

BiConsumer turns that action into a first-class, reusable value, and Map.forEach accepts it directly:

BiConsumer<String, Integer> printStock = (item, qty) -> System.out.println(item + ": " + qty);

stock.forEach(printStock);
Enter fullscreen mode Exit fullscreen mode

Practical Examples

1. Iterating a Map

Map<String, Integer> stock = Map.of("apples", 10, "bananas", 4);

stock.forEach((item, qty) -> System.out.println(item + ": " + qty));
Enter fullscreen mode Exit fullscreen mode

2. Logging Pairs

BiConsumer<String, Integer> logLowStock = (item, qty) -> {
    if (qty < 5) System.out.println("Low stock: " + item);
};

stock.forEach(logLowStock);
Enter fullscreen mode Exit fullscreen mode

3. Chaining with andThen

BiConsumer<String, Integer> print = (item, qty) -> System.out.println(item + ": " + qty);
BiConsumer<String, Integer> alert = (item, qty) -> {
    if (qty < 5) System.out.println("Alert! " + item + " is low");
};

BiConsumer<String, Integer> pipeline = print.andThen(alert);

pipeline.accept("bananas", 4);
// bananas: 4
// Alert! bananas is low
Enter fullscreen mode Exit fullscreen mode

4. Event Handling with Two Parameters

BiConsumer<Order, String> onStatusChange = (order, newStatus) ->
    System.out.println("Order " + order.getId() + " -> " + newStatus);

onStatusChange.accept(currentOrder, "SHIPPED");
Enter fullscreen mode Exit fullscreen mode

Real-World Patterns

  • Map Iteration: forEach over a Map is the most natural home for BiConsumer.
  • Contextual Callbacks: Passing both an event and its context to a handler in one call.
  • Coordinated Logging / Auditing: Recording two related pieces of information together, e.g. an entity and the action performed on it.
  • Two-Argument Event Handlers: Modeling listeners that need more than a single piece of data to act.

Best Practices

  • Isolate Side Effects: Keep a BiConsumer focused on one observable effect at a time.
  • Name Both Arguments' Role: onStatusChange, logLowStock communicate more than a generic handler.
  • Compose Carefully: Use andThen for a short, clear sequence of effects, not a long hidden chain.

Common Pitfalls

  • Thread Safety in Parallel forEach: A BiConsumer that mutates shared state can race when used with a parallel stream or a concurrent map.
  • Overuse for Transformation: If the goal is to produce a result, reach for BiFunction, don't smuggle a return value out through a captured variable.
  • Traceability: Multiple chained BiConsumers with side effects can make it hard to know, at a glance, everything a single accept() call actually does.

Functional Analogy

Think of BiConsumer as a two-person delivery:

  • Two packages arrive together, T and U.
  • Both get handled in the same visit.
  • Nothing is handed back, the effect is what mattered.

Conclusion

BiConsumer closes the loop on Java's core functional interfaces. Across ten episodes, we've covered every shape a single or paired value can take: a decision (Predicate, BiPredicate), a transformation (Function, BiFunction, UnaryOperator, BinaryOperator), an action (Consumer, BiConsumer), and a source (Supplier). Together, they form a small but complete vocabulary for functional Java.

What's Next

With all nine core interfaces covered, the next natural step is putting them to work together, composing predicates, functions, and consumers into a single real-world pipeline. That's where we're headed next: turning vocabulary into fluency. 🚀

Top comments (0)