Definition
- UnaryOperator
– T apply(T t): takes one T, returns one T. Same type in and out. Used byList.replaceAll. - BinaryOperator
– T apply(T a, T b): takes two T's, returns one T. All same type. Used byStream.reduce,Map.merge. - Why they exist – type clarity. Without them you would write
Function(redundant) orBiFunction(three identical types). - IS-A – UnaryOperator IS a Function; BinaryOperator IS a BiFunction. They are interchangeable where the types match.
UnaryOperator: apply 5% interest to a balance. Takes a BigDecimal, returns a BigDecimal (same type). BinaryOperator: add two balances together. Takes two BigDecimals, returns one BigDecimal (all same type). The Operator name signals: same type in, same type out.
Understanding the Concepts
What is UnaryOperator?
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.function.UnaryOperator;
// UnaryOperator<BigDecimal> = Function<BigDecimal, BigDecimal>
UnaryOperator<BigDecimal> applyInterest = balance ->
balance.multiply(new BigDecimal("1.05"));
// Used by List.replaceAll: transforms each element in-place
List<BigDecimal> balances = new ArrayList<>(List.of(
new BigDecimal("100000"), new BigDecimal("200000")));
balances.replaceAll(applyInterest);
// [105000, 210000]
// Static method:
UnaryOperator<String> identity = UnaryOperator.identity();
What is BinaryOperator?
import java.math.BigDecimal;
import java.util.Comparator;
import java.util.function.BinaryOperator;
// BinaryOperator<BigDecimal> = BiFunction<BigDecimal, BigDecimal, BigDecimal>
BinaryOperator<BigDecimal> sum = BigDecimal::add;
// Used by Stream.reduce
BigDecimal total = trades.stream()
.map(Trade::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// Used by Map.merge
java.util.Map<String, BigDecimal> totals = new java.util.HashMap<>();
totals.merge("RELIANCE", new BigDecimal("245050"), BigDecimal::add);
totals.merge("RELIANCE", new BigDecimal("123750"), BigDecimal::add);
// RELIANCE: 368800
Why do specializations exist?
// WITHOUT specialization: redundant type repetition
java.util.function.Function<BigDecimal, BigDecimal> transform = bal -> bal.multiply(rate);
java.util.function.BiFunction<BigDecimal, BigDecimal, BigDecimal> add = BigDecimal::add;
// WITH specialization: cleaner
UnaryOperator<BigDecimal> transform = bal -> bal.multiply(rate);
BinaryOperator<BigDecimal> add = BigDecimal::add;
What is the relationship to parent types?
| Specialization | Extends | Type constraint |
|---|---|---|
| UnaryOperator
| BinaryOperator
Both are IS-A relationships. Pass a UnaryOperator where Function
What are BinaryOperator.minBy and maxBy?
import java.util.Comparator;
import java.util.function.BinaryOperator;
BinaryOperator<BigDecimal> minOp = BinaryOperator.minBy(Comparator.naturalOrder());
BinaryOperator<BigDecimal> maxOp = BinaryOperator.maxBy(Comparator.naturalOrder());
// Use in reduce to find min/max
BigDecimal min = trades.stream().map(Trade::getAmount).reduce(minOp).orElse(BigDecimal.ZERO);
BigDecimal max = trades.stream().map(Trade::getAmount).reduce(maxOp).orElse(BigDecimal.ZERO);
What about primitive specializations?
import java.util.function.*;
// Primitive unary operators
IntUnaryOperator doubleIt = i -> i * 2;
LongUnaryOperator doubleItLong = l -> l * 2;
DoubleUnaryOperator doubleItDouble = d -> d * 2;
// Primitive binary operators
IntBinaryOperator addInts = Integer::sum;
LongBinaryOperator addLongs = Long::sum;
DoubleBinaryOperator addDoubles = Double::sum;
// Used in primitive stream operations
int sum = java.util.stream.IntStream.of(1, 2, 3).reduce(0, Integer::sum);
Where are they used in the JDK?
| API | Uses | Type |
|-----|------|------|
| List.replaceAll | UnaryOperator
| Stream.reduce | BinaryOperator
| Map.merge | BiFunction (often BinaryOperator) | Resolve duplicates |
| Stream.iterate | UnaryOperator
| Comparator.thenComparing | Function (often UnaryOperator) | Key extraction |
What should a senior engineer know?
- Both are IS-A relationships. Pass a UnaryOperator where Function
is expected. - Use UnaryOperator when the transformation keeps the same type. Use BinaryOperator when the reduction keeps the same type.
- Use plain Function/BiFunction when input and output types differ.
- There is no TernaryOperator. Define a custom functional interface for three arguments.
- Stream.iterate(seed, UnaryOperator) generates an infinite stream by repeatedly applying the operator.
Visual Overview
Type Specializations: Function<T, R> UnaryOperator<T> T → R (different types) T → T (same type) map(Trade::getAmount) List.replaceAll(applyInterest) BiFunction<T, U, R> BinaryOperator<T> (T, U) → R (T, T) → T (all same type) Map.compute Stream.reduce, Map.merge
Key Takeaway
UnaryOperator extends Function with the constraint that input and output are the same type. BinaryOperator extends BiFunction with all three types the same. They exist for API clarity: List.replaceAll(UnaryOperator) signals the element type does not change, and Stream.reduce(identity, BinaryOperator) signals all operands are the same type.
In a banking interest calculator, UnaryOperator applies 5% interest to each balance via List.replaceAll. BinaryOperator BigDecimal::add sums all balances via Stream.reduce and aggregates per symbol via Map.merge. BinaryOperator.minBy and maxBy create operators for comparisons.
Both are IS-A relationships: you can pass a UnaryOperator wherever Function
Remember one sentence: UnaryOperator is Function
---