Java 8 UnaryOperator and BinaryOperator Tutorial
Introduction
This tutorial explains UnaryOperator and BinaryOperator, two functional interfaces in Java 8'sjava.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>extendsFunction<T, T>: one argument of typeT, result of typeT. Trimming a string, raising a salary and doubling a number are unary operators.BinaryOperator<T>extendsBiFunction<T, T, T>: two arguments of typeT, result of typeT. Adding two numbers, picking the larger of two values and joining two strings are binary operators.
BinaryOperator<Integer> instead of BiFunction<Integer, Integer, Integer> - and tells the reader that the types match.
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()andcompose(): inherited from Function, so they return aFunction, not aUnaryOperator, even when both sides are operators. A method that needs aUnaryOperator- such asList.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)andmaxBy(...): 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 smallEmployee 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);
}
}
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:
tidytrims a name and collapses repeated spaces;shoutis the method referenceString::toUpperCase. Both take and return aString. - Part 2:
tidy.andThen(shout)works, but its type isFunction<String, String>, becauseandThen()is defined inFunction. - Part 3:
List.replaceAll()requires aUnaryOperator, so the combinedFunctionis adapted with a method reference to itsapplymethod: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::maxalso fitsBinaryOperator<Integer>; the one-argumentreduce()returns anOptional, 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, andminByused withreduce()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
BinaryOperatoron the value type. Tom and Nancy both fall in the "senior" band, so their names are joined.
Where the JDK expects operators
- UnaryOperator:
List.replaceAll(),Stream.iterate(),AtomicReference.updateAndGet()andgetAndUpdate(). - BinaryOperator:
Stream.reduce(), the merge functions ofCollectors.toMap()andMap.merge()(strictly a BiFunction there),Collectors.reducing(), the combiner of a custom CollectorClick to read tutorial: Java 8 Custom Collector with Collector.of() Tutorial,Arrays.parallelPrefix(), andAtomicReference.accumulateAndGet(). - Primitive versions:
IntUnaryOperator,IntBinaryOperatorand theirLongandDoublecounterparts avoid boxing - see primitive functional interfacesClick to read tutorial: Java 8 Primitive Functional Interfaces Tutorial.
Summary
In this tutorial we first saw thatUnaryOperator<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.Java 8 Tutorials
Java 8 BiFunction, BiConsumer and BiPredicate TutorialClick to read tutorial: Java 8 BiFunction, BiConsumer and BiPredicate Tutorial Java 8 Primitive Functional Interfaces TutorialClick to read tutorial: Java 8 Primitive Functional Interfaces Tutorial Java 8 CompletableFuture Tutorial with ExamplesClick to read tutorial: Java 8 CompletableFuture Tutorial with Examples Java 8 CompletableFuture: Combining and Error HandlingClick to read tutorial: Java 8 CompletableFuture: Combining and Error Handling Java 8 Parallel Streams Tutorial with ExamplesClick to read tutorial: Java 8 Parallel Streams Tutorial with Examples Java 8 Collectors.toMap Tutorial with ExamplesClick to read tutorial: Java 8 Collectors.toMap Tutorial with Examples Java 8 Custom Collector with Collector.of() TutorialClick to read tutorial: Java 8 Custom Collector with Collector.of() Tutorial
Java 8 BiFunction, BiConsumer and BiPredicate TutorialClick to read tutorial: Java 8 BiFunction, BiConsumer and BiPredicate Tutorial Java 8 Primitive Functional Interfaces TutorialClick to read tutorial: Java 8 Primitive Functional Interfaces Tutorial Java 8 CompletableFuture Tutorial with ExamplesClick to read tutorial: Java 8 CompletableFuture Tutorial with Examples Java 8 CompletableFuture: Combining and Error HandlingClick to read tutorial: Java 8 CompletableFuture: Combining and Error Handling Java 8 Parallel Streams Tutorial with ExamplesClick to read tutorial: Java 8 Parallel Streams Tutorial with Examples Java 8 Collectors.toMap Tutorial with ExamplesClick to read tutorial: Java 8 Collectors.toMap Tutorial with Examples Java 8 Custom Collector with Collector.of() TutorialClick to read tutorial: Java 8 Custom Collector with Collector.of() Tutorial