Java 8 UnaryOperator and BinaryOperator Tutorial

Introduction

This tutorial explains UnaryOperator and BinaryOperator, two functional interfaces in Java 8's java.util.function package that you meet constantly - in List.replaceAll(), Stream.iterate(), Stream.reduce() and the merge functions of collectors - often without noticing their names. We will start with what they are: special cases of Function and BiFunction whose argument and result types are all the same. We will then cover their methods, including the useful BinaryOperator.minBy() and maxBy(), and one surprise about andThen(). Lastly, we will run an example and list where the JDK expects them.

What are UnaryOperator and BinaryOperator

An operator takes values of one type and returns a value of the same type - like the + operator on numbers. In Java 8 terms -
  • UnaryOperator<T> extends Function<T, T>: one argument of type T, result of type T. Trimming a string, raising a salary and doubling a number are unary operators.
  • BinaryOperator<T> extends BiFunction<T, T, T>: two arguments of type T, result of type T. Adding two numbers, picking the larger of two values and joining two strings are binary operators.
Neither adds a new abstract method; they just fix the type parameters, which makes signatures shorter - BinaryOperator<Integer> instead of BiFunction<Integer, Integer, Integer> - and tells the reader that the types match.
Java 8 UnaryOperator extends Function and BinaryOperator extends BiFunction, and how reduce chains a BinaryOperator

Methods of UnaryOperator

  • T apply(T t): inherited from Function. Function descriptor: T -> T.
  • static <T> UnaryOperator<T> identity(): an operator that returns its argument unchanged - useful as a "do nothing" default.
  • andThen() and compose(): inherited from Function, so they return a Function, not a UnaryOperator, even when both sides are operators. A method that needs a UnaryOperator - such as List.replaceAll() - will not accept the combined result directly.

Methods of BinaryOperator

  • T apply(T t, T u): inherited from BiFunction. Function descriptor: (T, T) -> T.
  • static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) and maxBy(...): return an operator that gives back the smaller, or larger, of its two arguments according to the ComparatorClick to read: Comparator.
  • andThen(Function): inherited from BiFunction, returning a BiFunction.

Java 8 code example of UnaryOperator and BinaryOperator

The example works on employee names, salaries and leave days, using a small Employee class with a name and a salary.
Employee.java
//Employee.java
public class Employee {
  private final String name;
  private final double salary;

  public Employee(String name, double salary) {
    this.name = name;
    this.salary = salary;
  }

  public String getName() { return name; }
  public double getSalary() { return salary; }

  @Override
  public String toString() { return name + " (" + salary + ")"; }
}
Java 8 code showing UnaryOperator and BinaryOperator in replaceAll, iterate, reduce and toMap
//Operators.java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Operators {

  public static void main(String[] args) {
    // 1. UnaryOperator<T>: one T in, one T out
    UnaryOperator<String> tidy = name -> name.trim().replaceAll("\\s+", " ");
    UnaryOperator<String> shout = String::toUpperCase;
    System.out.println("1. [" + tidy.apply("  Tom    Jones ") + "] ["
        + shout.apply("tom") + "]");

    // 2. andThen() is inherited from Function, so it returns a Function, not a UnaryOperator
    Function<String, String> tidyThenShout = tidy.andThen(shout);
    System.out.println("2. " + tidyThenShout.apply("  harry   major"));

    // 3. List.replaceAll takes a UnaryOperator
    List<String> names = new ArrayList<>(Arrays.asList(" nancy smith", "ethan  hardy "));
    names.replaceAll(tidy.andThen(shout)::apply);
    System.out.println("3. " + names);

    // 4. Stream.iterate takes a UnaryOperator to make the next element
    List<Integer> doubling = Stream.iterate(1000, salary -> salary * 2).limit(4)
        .collect(Collectors.toList());
    System.out.println("4. " + doubling);

    // 5. UnaryOperator.identity() returns its argument unchanged
    UnaryOperator<String> same = UnaryOperator.identity();
    System.out.println("5. " + same.apply("Deborah Sprightly"));

    // 6. BinaryOperator<T>: two Ts in, one T out - the shape reduce() needs
    BinaryOperator<Integer> add = Integer::sum;
    List<Integer> leaveDays = Arrays.asList(3, 5, 2, 8);
    System.out.println("6. total leave: " + leaveDays.stream().reduce(0, add)
        + ", longest: " + leaveDays.stream().reduce(Integer::max).get());

    // 7. BinaryOperator.minBy / maxBy build operators from a Comparator
    List<Employee> staff = Arrays.asList(new Employee("Tom Jones", 5200),
        new Employee("Harry Major", 4100), new Employee("Nancy Smith", 5800));
    Comparator<Employee> bySalary = Comparator.comparingDouble(Employee::getSalary);
    BinaryOperator<Employee> higherPaid = BinaryOperator.maxBy(bySalary);
    BinaryOperator<Employee> lowerPaid = BinaryOperator.minBy(bySalary);
    System.out.println("7. higher paid of Tom and Harry: "
        + higherPaid.apply(staff.get(0), staff.get(1)).getName()
        + ", lowest paid overall: " + staff.stream().reduce(lowerPaid).get().getName());

    // 8. A BinaryOperator as the merge function of toMap()
    Map<String, String> namesByBand = staff.stream()
        .collect(Collectors.toMap(e -> e.getSalary() >= 5000 ? "senior" : "junior",
            Employee::getName, (first, second) -> first + " & " + second));
    System.out.println("8. " + namesByBand);
  }
}
 OUTPUT of the above code
1. [Tom Jones] [TOM]
2. HARRY MAJOR
3. [NANCY SMITH, ETHAN HARDY]
4. [1000, 2000, 4000, 8000]
5. Deborah Sprightly
6. total leave: 18, longest: 8
7. higher paid of Tom and Harry: Tom Jones, lowest paid overall: Harry Major
8. {senior=Tom Jones & Nancy Smith, junior=Harry Major}

Explanation of the code

  • Part 1: tidy trims a name and collapses repeated spaces; shout is the method reference String::toUpperCase. Both take and return a String.
  • Part 2: tidy.andThen(shout) works, but its type is Function<String, String>, because andThen() is defined in Function.
  • Part 3: List.replaceAll() requires a UnaryOperator, so the combined Function is adapted with a method reference to its apply method: tidy.andThen(shout)::apply. A lambda, s -> shout.apply(tidy.apply(s)), would work too. See the List.replaceAll tutorialClick to read: List.replaceAll tutorial.
  • Part 4: Stream.iterate(seed, operator) applies the operator to each element to get the next, so it needs input and output of the same type - see infinite streamsClick to read: infinite streams.
  • Part 5: UnaryOperator.identity() returns what it is given.
  • Part 6: reduce() feeds each result back in as the first argument of the next call, as the diagram shows - which only works if the operator's result has the same type as its inputs. Integer::max also fits BinaryOperator<Integer>; the one-argument reduce() returns an Optional, because the stream might be empty. See the reduce() tutorialClick to read: reduce() tutorial.
  • Part 7: BinaryOperator.maxBy(bySalary) picks the better-paid of two employees, and minBy used with reduce() finds the lowest-paid of all.
  • Part 8: the merge function of Collectors.toMap()Click to read tutorial: Java 8 Collectors.toMap Tutorial with Examples is a BinaryOperator on the value type. Tom and Nancy both fall in the "senior" band, so their names are joined.

Where the JDK expects operators

Summary

In this tutorial we first saw that UnaryOperator<T> is a Function<T, T> and BinaryOperator<T> is a BiFunction<T, T, T>, and why reduce() needs that matching type. Next we covered their methods: identity(), the inherited andThen() that returns a plain Function, and minBy() and maxBy(). We then ran an example using them in replaceAll(), iterate(), reduce() and toMap(). Lastly, we listed where the JDK expects operators and their primitive versions.