Java Predicate & BiPredicate Tutorial

Overview

The predicate interfaces in the java.util.function package return a boolean given one or some arguments.

The primary purpose of the Predicate interface is to simplify the task of evaluating conditions or tests within your Java applications. It abstracts the process of writing conditional statements and allows you to represent complex conditions as reusable, composable functions.

This post will give you a good understanding of the Predicate and BiPredicate functional interfaces.

The Predicate Interface

The Predicate interface has one abstract method apply with the following signature:

boolean test(T t);

In addition, there are some default methods I’ll show you later.

How To Create and use A Predicate

It’s quite simple to create and use a Predicate in Java. Let’s consider the following example:

record Vehicle(String type, int wheelsCount, int price) {
    public String toString() {
        return String.format("%s %s %d", type, wheelsCount, price);
    }
}

public class PredicateDemo {
    public static void main(String[] args) {
        Predicate<Vehicle> isACarPredicate = (Vehicle v) -> v.wheelsCount() == 4;

        var vehicle = new Vehicle("car", 4, 1000);

        System.out.println(isACarPredicate.test(vehicle));
    }
}

Running the code above would provide the following result:

Simple java predicate example

Predicates are usually used in a stream to filter data. Let’s consider the following example:

public static void main(String[] args) {
        Predicate<Vehicle> isACarPredicate = (Vehicle v) -> v.wheelsCount() == 4;

        var showroomVehicles = List.of(
                new Vehicle("Sport Car", 4, 10000),
                new Vehicle("Sedan Car", 4, 20000),
                new Vehicle("Dirt Bike", 2, 3000),
                new Vehicle("Toy car", 4, 30000),
                new Vehicle("Regular Bike", 2, 3000)
        );

        var onlyCars = showroomVehicles.stream()
                .filter(isACarPredicate)
                .toList();
        var onlyBikes = showroomVehicles.stream()
                .filter(isACarPredicate.negate())
                .toList();
        
        //print out
        System.out.println("Only cars: ");
        onlyCars.forEach(System.out::println);
        
        System.out.println("Only bikes: ");
        onlyBikes.forEach(System.out::println);

    }

As you can see, I used the predicate to filter out items I don’t want. To get only bikes, I can use the negate default method to achieve the result.

Running the code above would produce the following result:

Using predicate in stream

Predicate chaining

Let’s consider the following scenario, you want to find vehicles that is a car and and their prices is over 100,000.

You can do something like this:

    public static void main(String[] args) {

        var showroomVehicles = List.of(
                new Vehicle("Sport Car", 4, 200_000),
                new Vehicle("Sedan Car", 4, 20_000),
                new Vehicle("Oldest Toy Car", 4, 5_000_000),
                new Vehicle("F1 Car", 4, 2_000_000),
                new Vehicle("Dirt Bike", 2, 3_000),
                new Vehicle("Racing Dirt Bike", 2, 300_000),
                new Vehicle("Flying Bike", 2, 3_000_000),
                new Vehicle("Toy car", 4, 30_000),
                new Vehicle("Regular Bike", 2, 3000)
        );

        var expensiveCars = showroomVehicles.stream()
                .filter(vehicle -> vehicle.price() > 100_000 && vehicle.wheelsCount() == 4)
                .toList();

    }

That works fine. However, imagine later you also need to find the bikes that are expensive too.

You’ll need to write the filter again.

There is, in my opinion, a more interesting way to do both tasks with predicate.

Let’s see how it works:

public static void main(String[] args) {

        var showroomVehicles = List.of(
                new Vehicle("Sport Car", 4, 200_000),
                new Vehicle("Sedan Car", 4, 20_000),
                new Vehicle("Oldest Toy Car", 4, 5_000_000),
                new Vehicle("F1 Car", 4, 2_000_000),
                new Vehicle("Dirt Bike", 2, 3_000),
                new Vehicle("Racing Dirt Bike", 2, 300_000),
                new Vehicle("Flying Bike", 2, 3_000_000),
                new Vehicle("Toy car", 4, 30_000),
                new Vehicle("Regular Bike", 2, 3000)
        );

        Predicate<Vehicle> expensivePredicate = (Vehicle vehicle) -> vehicle.price() > 100_000;
        Predicate<Vehicle> carPredicate = (Vehicle vehicle) -> vehicle.wheelsCount() == 4;

        var expensiveCarsPredicate = carPredicate.and(expensivePredicate);
        var expensiveCars = showroomVehicles.stream().filter(expensiveCarsPredicate).toList();

        var expensiveBikesPredicate = expensivePredicate.and(carPredicate.negate());
        var expensiveBikes = showroomVehicles.stream().filter(expensiveBikesPredicate).toList();

    }

As you can see, on line 15 and 16, I created two predicates, one to test if a vehicle is expensive, one to test if a vehicle is a car.

Then on line 18, I created another predicate combining the previous two. Finally, I passed the newly created predicate to the filter.

Imagine you have more than two predicates, this style would make the code more readable.

BiPredicate

Similar to Predicate, BiPredicate has an abstract method named test that return a boolean. However, the test method of BiPredicate accepts two arguments, not one.

BiPredicate examples

Back in the early 2010s, there was a test that put Mentos into a Coke bottle and the Coke can exploded. Thus, people said that you must not drink Coke while eating Mentos.

Let’s use a BiPredicate to help people test if you can drink something while chewing something else.

First of all, let’s create some interfaces and records first:

interface Chewable {
}

interface Drinkable {
}

record Meat(String name) implements Chewable {
    @Override
    public String toString() {
        return name;
    }
}

record Mentos(String name) implements Chewable {
    @Override
    public String toString() {
        return name;
    }
}

record Water(String name) implements Drinkable {
    @Override
    public String toString() {
        return name;
    }
}

record Coke(String name) implements Drinkable {
    @Override
    public String toString() {
        return name;
    }
}

As you can see, there is nothing much. I created Chewable and Drinkable interfaces and some records that implement such interfaces.

Now let’s create and implement the predicate:

    private static void canIDrinkAndChew(Drinkable drink, Chewable chew) {
        BiPredicate<Drinkable, Chewable> canDrinkAndChewPredicate =
                (Drinkable drinkable, Chewable chewable) -> !(drinkable instanceof Coke && chewable instanceof Mentos);

        if (canDrinkAndChewPredicate.test(drink, chew)) {
            System.out.println("Yes, you can drink " + drink + " and chew " + chew + " and have a good time \uD83D\uDD25");
        } else {
            System.out.println("No!!!!!!!!! You cannot drink " + drink + " and chew " + chew + " \uD83D\uDC80");
        }
    }

This method creates a BiPredicate and test if you can drink and chew two things at the same time. As you can see, as long as drinkable is not Coke and chewable is not Mentos, there is no problem.

Let’s test this BiPredicate

    public static void main(String[] args) {
        var coke = new Coke("Coke");
        var water = new Water("Water");
        var mentos = new Mentos("Mentos");
        var meat = new Meat("Meat");


        canIDrinkAndChew(coke, mentos);
        canIDrinkAndChew(coke, meat);
        canIDrinkAndChew(water, mentos);
    }

Running the code would produce the following result:

As you can see, the end users now get a warning when trying to test Coke with Mentos. Otherwise, they are good to enjoy their drink and chew.

Conclusion

In this post, I’ve shown you how you can create and use Predicate and BiPredicate. Using these functional interfaces right can make your code clean, reusable, and easy to read.

Leave a Comment