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;
}
}
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;
}
With BinaryOperator, the combining rule becomes a reusable value:
BinaryOperator<Integer> sum = (a, b) -> a + b;
int total = prices.stream().reduce(0, sum);
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
2. minBy / maxBy
BinaryOperator<User> oldest = BinaryOperator.maxBy(Comparator.comparing(User::getAge));
User winner = oldest.apply(userA, userB);
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"
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;
};
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 cmust equala op (b op c), or results become order-dependent. -
Keep It Pure: A
BinaryOperatorshould compute a value, not mutate either argument. -
Name for the Combination:
sum,merge,pickNewestdescribe intent better thancombine.
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 aBinaryOperator.
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)