Back to Functional Interfaces
Q5Question 5 of 13

UnaryOperator vs BinaryOperator?

Key Concepts
  • UnaryOperator<T> extends Function<T,T>: one input, one output, same type
  • BinaryOperator<T> extends BiFunction<T,T,T>: two inputs, one output, all same type
  • They exist for type clarity: UnaryOperator signals the type does not change, avoiding redundant type parameters
  • UnaryOperator is used by List.replaceAll to transform each element in-place
  • BinaryOperator is used by Stream.reduce and Map.merge for accumulation
  • BinaryOperator.minBy and maxBy create operators that return the smaller or larger of two values
  • Both are IS-A relationships: UnaryOperator IS a Function, BinaryOperator IS a BiFunction

Definition

UnaryOperator<T> extends Function<T,T> for same-type transformations, and BinaryOperator<T> extends BiFunction<T,T,T> for same-type reductions.
  • UnaryOperator – T apply(T t): takes one T, returns one T. Same type in and out. Used by List.replaceAll.
  • BinaryOperator – T apply(T a, T b): takes two T's, returns one T. All same type. Used by Stream.reduce, Map.merge.
  • Why they exist – type clarity. Without them you would write Function (redundant) or BiFunction (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?

JAVA
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?

JAVA
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?

JAVA
// 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 | Function | Same input and output |
| BinaryOperator | BiFunction | All three types the same |

Both are IS-A relationships. Pass a UnaryOperator where Function is expected.

What are BinaryOperator.minBy and maxBy?

JAVA
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?

JAVA
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 | Transform in-place |
| Stream.reduce | BinaryOperator | Fold elements |
| Map.merge | BiFunction (often BinaryOperator) | Resolve duplicates |
| Stream.iterate | UnaryOperator | Generate next value |
| 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

DIAGRAM
  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 is expected. Use the Operator variants when types match; use plain Function/BiFunction when they differ.

Remember one sentence: UnaryOperator is Function and BinaryOperator is BiFunction, existing for type clarity when input and output are the same type.

---

Share
Use ← → arrow keys to navigate