Predefined Functional interfaces with 2 input arguments
-
Last Updated: August 18, 2023
-
By: javahandson
-
Series
Learn Java in a easy way
Learn predefined functional interfaces with 2 input arguments in Java 8. We cover BiPredicate, BiFunction, BiConsumer, and BinaryOperator, each with a clear example.
Predefined functional interfaces with 2 input arguments step in when a single input cannot do the job. You hand it two values. It runs your logic. Then you carry on.
Think about a simple task. You want to check whether two numbers add up to an even total. One value alone tells you nothing here. You need both before you can answer.
The one-argument interfaces cannot help you there. Predicate takes one value. Function takes one value. Consumer takes one value. Each of them stops at a single input.
So what do you reach for instead? Java 8 shipped a matching set of interfaces that accept two inputs. Their names all start with Bi, and they behave exactly like the versions you already know.
These show up far more often than beginners expect. Do you loop over a Map? Merge two totals? Check a name against a password? One of these interfaces sits under each of those jobs.
We start from the one-argument interfaces you already met, then step up to two. Here is the plan:
You do not need deep lambda knowledge to follow along. A rough idea of lambda expressions will carry you through every example here.
Three interfaces cover most single-input work. Each one takes a value and does something different with it.
Our article on predefined functional interfaces walks through all four in detail. Everything below builds directly on that.
Plenty of real questions need two pieces of information. A single input cannot express them.
Does this username match this password? Two values. What is the total of this price and this tax? Two values again. Which of these two orders came first? Same story.
You could squeeze both values into a small class or an array. That works, but it clutters your code with wrappers that carry no meaning of their own.
Java 8 chose the cleaner route. It added a second type parameter to each interface. Now you pass both values straight through.
The naming here could not be plainer. Bi means two, exactly as it does in bicycle or bilingual.
So BiPredicate equals Predicate plus one more input. BiFunction equals Function plus one more input. BiConsumer follows the same rule.
Better still, the method names never change. BiPredicate keeps test. BiFunction keeps apply. BiConsumer keeps accept. Only the argument count grows.
That consistency pays off. Once you know the one-argument trio, you already know most of what this article covers.
All of them sit in the java.util.function package, which arrived with Java 8. You import them the same way you import any other class.
import java.util.function.BiPredicate; import java.util.function.BiFunction; import java.util.function.BiConsumer; import java.util.function.BinaryOperator;
Each one carries the @FunctionalInterface annotation. That marker tells the compiler to allow exactly one abstract method, so a lambda can implement it.
Want to build your own? Our guide to the custom functional interface shows how that annotation works from the inside.
BiPredicate asks a yes-or-no question about two inputs. Its abstract method test accepts a T and a U, then returns a boolean.
@FunctionalInterface
public interface BiPredicate<T, U> {
boolean test(T t, U u);
}The two type parameters give you room to move. T and U can match each other, or they can differ completely.
Let us answer the question from the introduction. Do two numbers add up to an even total?
package com.javahandson.predefined;
import java.util.function.BiPredicate;
public class BiPredicateDemo {
public static void main(String[] args) {
BiPredicate<Integer, Integer> sumIsEven = (t, u) -> (t + u) % 2 == 0;
System.out.println(sumIsEven.test(10, 12)); // Output: true
System.out.println(sumIsEven.test(10, 11)); // Output: false
}
}Read the lambda from left to right. It takes t and u, adds them, then checks the remainder against zero.
Nothing about the two inputs forces them to share a type. This next one pairs a String with an Integer.
BiPredicate<String, Integer> hasLength = (text, size) -> text.length() == size;
System.out.println(hasLength.test("java", 4)); // Output: true
System.out.println(hasLength.test("handson", 4)); // Output: falseBiPredicate ships three default methods that combine checks. They mirror the ones on Predicate.
BiPredicate<Integer, Integer> bothPositive = (a, b) -> a > 0 && b > 0; BiPredicate<Integer, Integer> sumOverTen = (a, b) -> a + b > 10; System.out.println(bothPositive.and(sumOverTen).test(6, 7)); // Output: true System.out.println(bothPositive.and(sumOverTen).test(2, 3)); // Output: false System.out.println(bothPositive.or(sumOverTen).test(2, 3)); // Output: true System.out.println(sumOverTen.negate().test(2, 3)); // Output: true
Notice how and short-circuits. When the first predicate fails, the second one never runs at all.
Here is the shape you meet in real code. A login check compares two strings and answers with a boolean.
BiPredicate<String, String> validLogin =
(user, pass) -> user.equals("admin") && pass.length() >= 8;
System.out.println(validLogin.test("admin", "secret123")); // Output: true
System.out.println(validLogin.test("admin", "short")); // Output: false
System.out.println(validLogin.test("guest", "secret123")); // Output: falseOne line of logic, no wrapper class, no boilerplate. That readability explains why these interfaces spread so quickly through modern Java code.
BiFunction takes two inputs and produces one result. Notice the third type parameter, which the predicate never needed.
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
}Three type parameters sound heavy, but each one earns its place.
All three can differ. You might feed in a String and an Integer, then return a Boolean.
Multiplication makes an easy starting point. Two numbers go in, one number comes out.
package com.javahandson.predefined;
import java.util.function.BiFunction;
public class BiFunctionDemo {
public static void main(String[] args) {
BiFunction<Integer, Integer, Integer> multiply = (t, u) -> t * u;
System.out.println(multiply.apply(10, 12)); // Output: 120
BiFunction<String, Integer, String> repeat = (text, times) -> {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < times; i++) {
sb.append(text);
}
return sb.toString();
};
System.out.println(repeat.apply("ab", 3)); // Output: ababab
}
}The second lambda mixes types freely. A String and an int go in, and a String comes back out.
BiFunction offers one default method called andThen. It runs a plain Function over whatever your BiFunction produced.
import java.util.function.BiFunction; import java.util.function.Function; BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b; Function<Integer, String> label = total -> "Total is " + total; BiFunction<Integer, Integer, String> addThenLabel = add.andThen(label); System.out.println(addThenLabel.apply(20, 22)); // Output: Total is 42
Order matters here. The BiFunction runs first, then its result flows into the Function.
Notice the return type shifting along the chain. Two Integers enter, an Integer appears in the middle, and a String leaves at the end.
Function gives you both andThen and compose. BiFunction gives you andThen alone. Why the difference?
Think about what compose would need to do. It runs a function before the main one, feeding it the input.
A BiFunction takes two inputs, though. So a function running ahead of it must hand back two values at once. No plain Function can do that.
So the JDK skips compose here. Only andThen makes sense, because a single result flows out no matter how many inputs went in.
BiConsumer takes two inputs and returns nothing. Its abstract method accept has a void return type.
@FunctionalInterface
public interface BiConsumer<T, U> {
void accept(T t, U u);
}Nothing comes back, so a BiConsumer earns its keep through side effects. It prints, it logs, it writes to a file, or it updates something outside itself.
Only two type parameters appear this time. With no result to describe, R would serve no purpose.
Let us print a product name alongside its price.
package com.javahandson.predefined;
import java.util.function.BiConsumer;
public class BiConsumerDemo {
public static void main(String[] args) {
BiConsumer<String, Double> printPrice =
(name, price) -> System.out.println(name + " costs " + price);
printPrice.accept("Keyboard", 24.99); // Output: Keyboard costs 24.99
printPrice.accept("Monitor", 189.50); // Output: Monitor costs 189.5
}
}Try to assign the call to a variable and the compiler stops you at once. Nothing flows out of accept.
BiConsumer also carries an andThen method, but it behaves a little differently. It accepts a second BiConsumer, not a Function.
BiConsumer<String, Double> show =
(name, price) -> System.out.println(name + " costs " + price);
BiConsumer<String, Double> audit =
(name, price) -> System.out.println("Logged: " + name);
show.andThen(audit).accept("Mouse", 15.0);
// Output: Mouse costs 15.0
// Output: Logged: MouseBoth consumers see the very same pair of inputs. The first runs, the second follows, and neither passes anything to the other.
Here is where most developers meet BiConsumer without realising it. The forEach method on Map takes one.
import java.util.HashMap;
import java.util.Map;
Map<String, Integer> scores = new HashMap<>();
scores.put("Asha", 91);
scores.put("Ravi", 78);
scores.forEach((name, score) -> System.out.println(name + " scored " + score));
// Output: Asha scored 91
// Output: Ravi scored 78A Map entry holds two things, a key and a value. So the callback naturally needs two inputs, which makes BiConsumer the perfect fit.
Several other Map methods lean on these interfaces too:
Often all three types line up. You add two ints and get an int. You join two Strings and get a String.
Writing BiFunction<Integer, Integer, Integer> every time grows tiresome. BinaryOperator trims that down to a single type parameter.
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T, T, T> {
// inherits R apply(T t, U u), with T, U and R all fixed to T
}
BinaryOperator<Integer> add = (a, b) -> a + b;
System.out.println(add.apply(20, 22)); // Output: 42Read that extends clause carefully. BinaryOperator does not invent anything new, it simply pins all three type parameters to T.
BinaryOperator adds two static helpers. Each one takes a Comparator and returns an operator that picks a winner.
import java.util.Comparator;
import java.util.function.BinaryOperator;
BinaryOperator<String> shorter =
BinaryOperator.minBy(Comparator.comparingInt(String::length));
BinaryOperator<String> longer =
BinaryOperator.maxBy(Comparator.comparingInt(String::length));
System.out.println(shorter.apply("java", "handson")); // Output: java
System.out.println(longer.apply("java", "handson")); // Output: handsonThe reduce method on Stream expects a BinaryOperator. That requirement makes sense once you picture the work.
Reduce folds a whole stream into one value. It grabs two elements, combines them, then repeats with the result and the next element.
import java.util.stream.Stream; int total = Stream.of(1, 2, 3, 4, 5).reduce(0, (a, b) -> a + b); System.out.println(total); // Output: 15
Since the running total and each element share a type, only a BinaryOperator will do. Collectors.toMap uses one the same way, to settle duplicate keys.
Our article on the Collectors class digs further into that pattern.
Search the JDK for BiSupplier and you find nothing. No such interface exists, and that gap is deliberate.
Recall what Supplier does. It accepts zero arguments and produces a value through get.
Now put Bi in front of it. Bi promises two inputs, yet a supplier accepts none. The two ideas contradict each other outright.
Picture the two ways it could go. Give it two inputs and it stops acting as a supplier. Give it none and the Bi prefix turns into a lie. Either way, the name breaks.
Sometimes you genuinely want two values back. A few clean options cover that need:
Interviewers ask about BiSupplier surprisingly often. They want to hear the reasoning, not just the word no.
Generics refuse primitives. BiFunction<Integer, Integer, Integer> therefore wraps every int in an Integer object.
That wrapping carries a price. Java allocates an object, then unwraps it again on the way out, over and over.
For a handful of calls, nobody notices. Inside a loop running millions of times, the cost turns real.
So the JDK ships primitive-flavoured versions. They work on int, long, and double directly.
import java.util.function.IntBinaryOperator;
import java.util.function.ToIntBiFunction;
IntBinaryOperator addInts = (a, b) -> a + b;
System.out.println(addInts.applyAsInt(20, 22)); // Output: 42
ToIntBiFunction<String, String> totalLength = (x, y) -> x.length() + y.length();
System.out.println(totalLength.applyAsInt("java", "handson")); // Output: 11One gap deserves a mention. No primitive BiPredicate exists, so a two-int condition still goes through BiPredicate<Integer, Integer>.
Here is the whole family on one screen. Keep this table handy while the names settle in.
| Interface | Method | Inputs | Returns | Typical use |
|---|---|---|---|---|
| BiPredicate<T, U> | test | T and U | boolean | Conditional check on a pair |
| BiFunction<T, U, R> | apply | T and U | R | Combine two values into one |
| BiConsumer<T, U> | accept | T and U | void | Print, log, or store a pair |
| BinaryOperator<T> | apply | T and T | T | Reduce, merge, pick a winner |
| BiSupplier | – | – | – | Does not exist in Java |
One question sorts them fast. Ask what comes out. A boolean points to BiPredicate. Nothing at all points to BiConsumer. Any other value points to BiFunction.
Writing BiFunction<Integer, Integer, Integer> compiles perfectly well. Still, BinaryOperator<Integer> says the same thing in far fewer words.
The distinction bites when an API demands the narrower type. Stream.reduce wants a BinaryOperator, and a plain BiFunction will not slot in.
Beginners often try to capture a result from accept. The compiler rejects that straight away.
BiConsumer<String, Integer> c = (name, age) -> System.out.println(name + age);
// Compile error: void cannot convert to String
// String result = c.accept("Asha", 30);
c.accept("Asha", 30); // Output: Asha30Need a value back? Switch to BiFunction. Reach for BiConsumer only when a side effect is the entire point.
The first lambda parameter always maps to T, and the second always maps to U. Swap them by accident and the types may still compile.
BiFunction<Integer, Integer, Integer> subtract = (a, b) -> a - b; System.out.println(subtract.apply(10, 3)); // Output: 7 System.out.println(subtract.apply(3, 10)); // Output: -7
Both calls compile cleanly, yet only one answers your question. Order matters most with subtraction, division, and comparison.
Running BiFunction<Integer, Integer, Integer> a few thousand times costs nothing worth measuring. Push that into the millions and the wrappers pile up.
Swap in IntBinaryOperator when the numbers get big. Profile first, though, since guessing about performance wastes more time than it saves.
Short lambdas read beautifully until one quietly ignores an input. A BiPredicate that never touches its second argument usually signals a mistake.
// Suspicious: u never appears in the body BiPredicate<Integer, Integer> check = (t, u) -> t > 0; System.out.println(check.test(5, -100)); // Output: true
Read every lambda once more before you move on. Both parameters should earn their place, or the second one has no reason to exist.
Let us pull all four interfaces into one small program. We will build a tiny sales report.
The program tracks totals per region. It adds new sales to the running total. It flags the strong regions. Then it prints a short summary.
package com.javahandson.predefined;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.BinaryOperator;
public class SalesReport {
public static void main(String[] args) {
Map<String, Integer> sales = new HashMap<>();
// BinaryOperator settles a clash between an old and a new total
BinaryOperator<Integer> sum = (oldValue, newValue) -> oldValue + newValue;
sales.merge("North", 300, sum);
sales.merge("South", 150, sum);
sales.merge("North", 200, sum); // 300 + 200
// BiPredicate decides which regions did well
BiPredicate<String, Integer> strongRegion = (region, total) -> total >= 400;
// BiFunction turns a pair into one printable line
BiFunction<String, Integer, String> asLine =
(region, total) -> region + " sold " + total + " units";
// BiConsumer prints each line, with a star for the strong regions
BiConsumer<String, Integer> report = (region, total) -> {
String line = asLine.apply(region, total);
System.out.println(strongRegion.test(region, total) ? line + " *" : line);
};
sales.forEach(report);
}
}
Output : North sold 500 units *
South sold 150 unitsWalk through what each interface contributed:
Every piece of logic lives in its own small lambda. You can swap the threshold or the wording without touching anything else.
That separation gives these interfaces their real value. Each one names a job, and the name tells you what to expect. To see the same style applied to streams, try our guide to filtering in streams.
A: They are the java.util.function interfaces that accept two inputs instead of one. The main four are BiPredicate, BiFunction, BiConsumer, and BinaryOperator. Each mirrors its single-argument version, keeps the same method name, and simply adds a second parameter.
A: Predicate declares test(T t) and checks one value. BiPredicate declares test(T t, U u) and checks two. Both return a boolean, and both offer the and, or, and negate default methods. Only the input count differs.
A: Supplier takes zero arguments and returns a value. The Bi prefix promises two arguments, which directly contradicts that contract. A BiSupplier would break the supplier idea, or else turn its own name into a lie. So the JDK never shipped one.
A: BinaryOperator<T> extends BiFunction<T, T, T>. So a BinaryOperator is a BiFunction whose two inputs and result all share one type. Use BiFunction when the types differ, and BinaryOperator when they match. Methods such as Stream.reduce require the narrower BinaryOperator.
A: compose would run a function before the main one and feed it the input. A BiFunction needs two inputs, and an ordinary Function cannot produce two values. Since only one result flows out, andThen still works, so the JDK provides that method alone.
A: Map.forEach takes a BiConsumer over the key and the value. A map entry holds two pieces of data, so the callback needs two parameters and returns nothing. Map.replaceAll, merge, compute, and computeIfPresent all take a BiFunction instead.
A: Yes, for BiPredicate, BiFunction, and BiConsumer. Their type parameters T and U stay independent, so BiFunction<String, Integer, Boolean> compiles fine. BinaryOperator forms the exception, because it locks both inputs and the result to a single type T.
A: Pick a primitive-specialised interface. IntBinaryOperator, LongBinaryOperator, and DoubleBinaryOperator work on raw values, while ToIntBiFunction and ToDoubleBiFunction take objects and return a primitive. Note that the JDK ships no primitive BiPredicate.
A: It chains a second BiConsumer after the first. Both receive the identical pair of inputs, and neither passes anything to the other. That differs from BiFunction.andThen, which feeds its result into a following Function.
A: Yes. Comparator declares compare(T o1, T o2), which takes two inputs and returns an int. It lives in java.util, not java.util.function. Even so, it has just one abstract method, so lambdas work fine.
Let us wrap up what we covered. Predefined functional interfaces with 2 input arguments extend the one-argument trio. Each one takes a pair of values instead of just one.
BiPredicate answers a yes-or-no question through test. BiFunction folds two values into one result through apply. BiConsumer acts on a pair through accept and hands nothing back.
BinaryOperator narrows BiFunction to a single type, which is why reduce and merge insist on it. And BiSupplier never existed, because a supplier accepts no arguments at all.
Ask one question when you need to choose. What comes out of the method? That answer points you at the right interface almost every time.
Reach for the primitive versions once your loops grow hot. Everywhere else, favour the plain generic forms and keep your lambdas short and honest.