DEV Community

Cover image for Surfing with FP Java - Mastering BinaryOperator<T>
André Borba
André Borba

Posted on

Surfing with FP Java - Mastering BinaryOperator<T>

Introduction

In the last episode, we mastered UnaryOperator, the specialist that transforms one value into another of the same type.

Now, let's go one step further with BinaryOperator: it takes two values of the same type and combines them into a single result, still of that type.

If UnaryOperator refines, BinaryOperator merges.

What Is BinaryOperator?

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T, T, T> {

    static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
        return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
    }

    static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
        return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
    }
}
Enter fullscreen mode Exit fullscreen mode

Input: two objects, both of type T.
Output: one object, also of type T.
Purpose: combine two values into a single value of the same type, the backbone of reduction.

Why Use BinaryOperator?

Before Java 8, combining values meant a loop with a mutable accumulator:

int total = 0;
for (int price : prices) {
    total = total + price;
}
Enter fullscreen mode Exit fullscreen mode

With BinaryOperator, the combining rule becomes a reusable value:

BinaryOperator<Integer> sum = (a, b) -> a + b;

int total = prices.stream().reduce(0, sum);
Enter fullscreen mode Exit fullscreen mode

Now sum can be tested, named, and reused, independent of any particular loop.

Practical Examples

1. Reduce with an Explicit BinaryOperator

BinaryOperator<Integer> sum = Integer::sum;

int total = List.of(10, 20, 30).stream()
    .reduce(0, sum);

System.out.println(total); // 60
Enter fullscreen mode Exit fullscreen mode

2. minBy / maxBy

BinaryOperator<User> oldest = BinaryOperator.maxBy(Comparator.comparing(User::getAge));

User winner = oldest.apply(userA, userB);
Enter fullscreen mode Exit fullscreen mode

3. String Merging

BinaryOperator<String> join = (a, b) -> a + ", " + b;

String csv = List.of("java", "kotlin", "clojure").stream()
    .reduce(join)
    .orElse("");

System.out.println(csv); // "java, kotlin, clojure"
Enter fullscreen mode Exit fullscreen mode

4. Custom Combiners for Parallel Streams

BinaryOperator<Map<String, Integer>> mergeCounts = (m1, m2) -> {
    Map<String, Integer> merged = new HashMap<>(m1);
    m2.forEach((k, v) -> merged.merge(k, v, Integer::sum));
    return merged;
};
Enter fullscreen mode Exit fullscreen mode

Real-World Patterns

  • Aggregation: Totals, averages, and counts built on top of reduce.
  • Merging: Combining two partial results from parallel computations back into one.
  • Tie-Breaking: Picking a winner between two candidates with minBy / maxBy.
  • Custom Combiners: Supplying the combiner argument in the three-argument overload of reduce, needed for parallel streams.

Best Practices

  • Keep It Associative: Especially for parallel streams, (a op b) op c must equal a op (b op c), or results become order-dependent.
  • Keep It Pure: A BinaryOperator should compute a value, not mutate either argument.
  • Name for the Combination: sum, merge, pickNewest describe intent better than combine.

Common Pitfalls

  • Non-Associative Operations: Subtraction or division break parallel reduce, since the JVM may group operations differently across threads.
  • Missing Identity Element: Forgetting a sensible seed value for reduce(identity, operator) can produce wrong results for empty inputs.
  • Confusing It with BiFunction: If the two inputs and the output aren't all the same type, that's a BiFunction, not a BinaryOperator.

Functional Analogy

Think of BinaryOperator as a river confluence:

  • Two streams of the same kind of water arrive.
  • They merge at one point.
  • A single stream of that same water continues onward.

Conclusion

BinaryOperator anchors reduction and combination logic in functional Java. It's the quiet engine behind sum, max, min, and every reduce call that folds a collection into a single value.

What's Next

In the next episode, we'll relax the same-type constraint entirely and meet BiFunction, an interface built for combining two values of different types into a result of a third type.

Top comments (0)