Beyond the four core interfaces, what are the arity-2 variants (BiFunction, BiConsumer, BiPredicate) and the operator specializations (UnaryOperator, BinaryOperator)? How do they relate to the core types?
answer
- Bi- = two arguments; no BiSupplier (supplier takes none)
- UnaryOperator<T> = Function<T,T>; BinaryOperator<T> = BiFunction<T,T,T>
- reduce / Map.merge take BinaryOperator
- List.replaceAll takes UnaryOperator; Map.forEach takes BiConsumer
- Operators EXTEND the Function types — substitutable upward, not freely downward
basics
~20 sThe Bi- versions take two arguments instead of one: BiFunction<T,U,R>, BiConsumer<T,U>, BiPredicate<T,U>. UnaryOperator<T> is a Function whose input and output are the same type; BinaryOperator<T> is a BiFunction with both inputs and output the same type.
solid answer
~40 sThe catalog scales the core shapes along two axes. First, arity: BiFunction<T,U,R> takes two inputs and returns R; BiConsumer<T,U> takes two and returns void; BiPredicate<T,U> takes two and returns boolean. (There is deliberately no BiSupplier — a supplier takes no input, so arity-2 is meaningless.) Second, same-type specializations: UnaryOperator<T> extends Function<T,T> for a one-argument operation whose result type equals its input type, and BinaryOperator<T> extends BiFunction<T,T,T> for a two-argument operation over a single type. These operators are not new shapes; they are narrowings that read better and let APIs express intent — for example Map.merge and Stream.reduce take a BinaryOperator because reduction combines two values of one type into one. Using UnaryOperator<String> instead of Function<String,String> signals 'transforms a string into a string' at the type level.
code
java · 10 linesBiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
BiConsumer<String, Integer> log = (name, age) -> System.out.println(name + "=" + age);
BiPredicate<String, String> startsWith = String::startsWith;
UnaryOperator<String> upper = String::toUpperCase; // Function<String,String>
BinaryOperator<Integer> max = BinaryOperator.maxBy(Integer::compareTo);
// BinaryOperator is exactly what reduce wants:
int total = java.util.stream.Stream.of(1, 2, 3).reduce(0, Integer::sum);
Function<String,String> asFn = upper; // OK: UnaryOperator IS-A Function<T,T>go deeper
Knows the Bi- prefix means two arguments and that UnaryOperator/BinaryOperator are same-type shorthands.
Correctly states the generic parameters (BiFunction<T,U,R>, BinaryOperator<T>) and names a real API for each (Map.merge, Stream.reduce, List.replaceAll).
Explains the subtype relationship (operators extend the Function types) and the substitutability direction, and why reduction signatures use BinaryOperator.
Articulates the design rationale — minimal core plus two orthogonal axes, the deliberate arity cap at 2, and operators as expressiveness aliases — and can reason about API ergonomics when extending it.
## The two axes of the catalog `java.util.function` is organized so that the **four core shapes** (Function, Consumer, Supplier, Predicate) are systematically extended along two independent axes: **arity** (how many arguments) and **type uniformity** (whether the input(s) and output share a type). Understanding the axes means you can name almost any interface in the package without memorizing a list. ## Axis 1 — arity (the Bi- family) The core interfaces take **one** argument. The `Bi` prefix means **two** arguments: | Interface | Method | Shape | |---|---|---| | `BiFunction<T,U,R>` | `R apply(T, U)` | two inputs → a value | | `BiConsumer<T,U>` | `void accept(T, U)` | two inputs → void | | `BiPredicate<T,U>` | `boolean test(T, U)` | two inputs → boolean | Note `T` and `U` can be different types. There is **no `BiSupplier`**: a `Supplier` takes *no* argument by definition, so 'two arguments' is nonsensical. Java also stops at arity 2 — if you need three arguments you must declare your own interface or restructure (e.g. pass a record). This is a deliberate boundary to keep the package small. You meet the Bi- family in map operations: `Map.forEach((key, value) -> ...)` takes a `BiConsumer`; `Map.merge` and `Map.compute` take a `BiFunction`. ## Axis 2 — same-type operators When a transformation's **input and output share a type**, the package offers narrower names: - **`UnaryOperator<T> extends Function<T,T>`** — one argument of type `T`, result of type `T`. Example: `UnaryOperator<String> upper = String::toUpperCase;`. It *is* a `Function<T,T>` — it just renames it for clarity. `List.replaceAll` takes a `UnaryOperator`. - **`BinaryOperator<T> extends BiFunction<T,T,T>`** — two arguments of type `T`, result of type `T`. Example: `BinaryOperator<Integer> sum = Integer::sum;`. `Stream.reduce`, `Map.merge`, and `Collectors` use `BinaryOperator` because **reduction** inherently combines two values of one type into one value of that same type. `BinaryOperator` also offers the static helpers `minBy(Comparator)` and `maxBy(Comparator)`. Because `UnaryOperator`/`BinaryOperator` **extend** the corresponding Function type, anywhere a `Function<T,T>` is accepted you can pass a `UnaryOperator<T>`, and a `UnaryOperator` literal can be assigned to a `Function<T,T>` variable — but **not** the reverse without a cast, since not every `Function<T,T>` is declared as a `UnaryOperator`. ## Why bother with the operator names They are pure **expressiveness**: `BinaryOperator<Integer>` communicates 'combine two ints into an int' more precisely than `BiFunction<Integer,Integer,Integer>`, and it shortens the generic signature. The runtime behavior is identical. ## Deriving an answer Start from a core shape, then apply the axes. 'Two strings to a boolean' → start at Predicate, add arity → `BiPredicate<String,String>`. 'Combine two ints into an int' → start at Function (returns a value), it has two same-type inputs and a same-type output → `BinaryOperator<Integer>`. 'Transform a date to a date' → one same-type input/output → `UnaryOperator<LocalDate>`.
- Why does the package not include a BiSupplier?A Supplier is defined as taking no input and producing a value; arity is about input count, so 'two-input supplier' is a contradiction. There is nothing to be Bi about.
- If a method parameter is Function<String,String>, can you pass a UnaryOperator<String>? And vice versa?Yes for UnaryOperator → Function, because UnaryOperator<T> extends Function<T,T>, so it is a subtype. The reverse needs an explicit cast or a re-wrap, since a general Function<T,T> is not declared as a UnaryOperator.
saying these in an interview costs you the question
- Inventing 'BiSupplier' — it does not and cannot exist (supplier has no input)
- Saying BinaryOperator can have different input and output types — all three are the same T
- Thinking the catalog provides tri-arity (TriFunction) — Java stops at Bi-
- Believing UnaryOperator and Function<T,T> are unrelated — UnaryOperator extends Function<T,T>