Table of Contents

Predefined Functional interfaces with 2 input arguments

  • Last Updated: August 18, 2023
  • By: javahandson
  • Series
img

Predefined Functional interfaces with 2 input arguments

Learn predefined functional interfaces with 2 input arguments in Java 8. We cover BiPredicate, BiFunction, BiConsumer, and BinaryOperator, each with a clear example.

1. Introduction

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.

1.1 What This Article Covers

We start from the one-argument interfaces you already met, then step up to two. Here is the plan:

  • Why a single input runs out of road so quickly
  • BiPredicate, and how it answers true or false about two values
  • BiFunction, and how it folds two values into one result
  • BiConsumer, and why looping a Map depends on it
  • BinaryOperator, the special case that powers reduce
  • Why the JDK ships no BiSupplier at all
  • Primitive versions that skip the cost of boxing
  • Common mistakes, plus the interview questions that follow

You do not need deep lambda knowledge to follow along. A rough idea of lambda expressions will carry you through every example here.

2. Why Two Arguments Change Things

2.1 A Quick Recap of the One-Argument Trio

Three interfaces cover most single-input work. Each one takes a value and does something different with it.

  • Predicate<T> answers a yes-or-no question. Its method test returns a boolean.
  • Function<T, R> transforms a value. Its method apply returns a result of some other type.
  • Consumer<T> acts on a value and hands nothing back. Its method accept returns void.
  • Supplier<T> sits apart from the rest. It takes nothing and produces a value through get.

Our article on predefined functional interfaces walks through all four in detail. Everything below builds directly on that.

2.2 Where One Argument Falls Short

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.

2.3 The Bi Prefix Simply Means Two

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.

2.4 Where These Interfaces Live

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.

3. BiPredicate: A Question About Two Values

3.1 The Shape of the Interface

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.

  • t holds the first input, of any type you pick for T
  • u holds the second input, of any type you pick for U
  • Returns true when your condition holds, and false otherwise

3.2 Your First BiPredicate

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: false

3.3 Joining Checks With and, or, and negate

BiPredicate ships three default methods that combine checks. They mirror the ones on Predicate.

  • and passes only when both predicates return true
  • or passes when either predicate returns true
  • negate flips the result of a single 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.

3.4 A Real Example: Checking a Login

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: false

One line of logic, no wrapper class, no boilerplate. That readability explains why these interfaces spread so quickly through modern Java code.

4. BiFunction: Turn Two Values Into One

4.1 The Shape of the BiFunction Interface

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.

  • T sets the type of the first input
  • U sets the type of the second input
  • R sets the type that apply hands back

All three can differ. You might feed in a String and an Integer, then return a Boolean.

4.2 Your First BiFunction

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.

4.3 Chaining With andThen

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.

4.4 Why There Is No compose Method

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.

5. BiConsumer: Act on Two Values

5.1 The Shape of the BiConsumer Interface

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.

5.2 Your First BiConsumer

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.

5.3 Chaining Two BiConsumers

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: Mouse

Both consumers see the very same pair of inputs. The first runs, the second follows, and neither passes anything to the other.

5.4 The Everyday Use: Looping Over a Map

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 78

A 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:

  • forEach accepts a BiConsumer over each key and value
  • replaceAll accepts a BiFunction and stores whatever it returns
  • merge accepts a BiFunction to settle a clash between an old and a new value
  • compute and computeIfPresent both accept a BiFunction over the key and current value

6. BinaryOperator: A BiFunction With One Type

6.1 When Both Inputs and the Result Match

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: 42

Read that extends clause carefully. BinaryOperator does not invent anything new, it simply pins all three type parameters to T.

6.2 The minBy and maxBy Helpers

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: handson

6.3 Where You Meet It in Streams

The 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.

7. Why There Is No BiSupplier

7.1 The Reason in One Line

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.

7.2 What to Use Instead

Sometimes you genuinely want two values back. A few clean options cover that need:

  • Return a small record or class holding both values, then supply that instead
  • Return a Map.Entry when a simple key-and-value pair says everything
  • Use two separate Supplier instances when the values have nothing in common

Interviewers ask about BiSupplier surprisingly often. They want to hear the reasoning, not just the word no.

8. The Primitive Cousins That Skip Boxing

8.1 Why Boxing Costs You

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.

8.2 The Two-Argument Primitive Interfaces

So the JDK ships primitive-flavoured versions. They work on int, long, and double directly.

  • ToIntBiFunction<T, U> takes two objects and returns a raw int
  • ToLongBiFunction<T, U> and ToDoubleBiFunction<T, U> follow the same idea
  • IntBinaryOperator takes two ints and returns an int, with no wrapper anywhere
  • LongBinaryOperator and DoubleBinaryOperator match that pattern
  • ObjIntConsumer<T> takes an object plus a raw int and returns nothing
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: 11

One gap deserves a mention. No primitive BiPredicate exists, so a two-int condition still goes through BiPredicate<Integer, Integer>.

9. Comparing the Four Interfaces

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.

10. Common Mistakes and Pitfalls

10.1 Reaching for a BiFunction When You Want a BinaryOperator

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.

10.2 Forgetting That BiConsumer Returns Nothing

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: Asha30

Need a value back? Switch to BiFunction. Reach for BiConsumer only when a side effect is the entire point.

10.3 Mixing Up the Argument Order

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.

10.4 Boxing in a Hot Loop

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.

10.5 Writing a Lambda That Hides a Bug

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.

11. A Practical Walkthrough

11.1 The Task

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.

11.2 The Code

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 units

11.3 Reading the Result

Walk through what each interface contributed:

  • BinaryOperator merged 300 and 200 into 500 for the North region
  • BiPredicate tested each region against the 400 threshold
  • BiFunction built one readable line from a region and its total
  • BiConsumer printed each line, adding a star where it belonged

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.

12. Interview Questions

Q: What are predefined functional interfaces with 2 input arguments in Java?

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.

Q: What is the difference between Predicate and BiPredicate?

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.

Q: Why does BiSupplier not exist in Java?

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.

Q: What is the difference between BiFunction and BinaryOperator?

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.

Q: Why does BiFunction have andThen but not compose?

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.

Q: Which functional interface does Map.forEach use?

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.

Q: Can the two input arguments have different types?

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.

Q: How do I avoid boxing with two-argument functional interfaces?

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.

Q: What does BiConsumer.andThen do?

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.

Q: Is Comparator a two-argument functional interface?

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.

13. Conclusion

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.

Further Reading

Leave a Comment