Functional Programming in Java 8: Definitive Guide to Predicates

Functional Programming in Java 8: Definitive Guide to Predicates

Introduction

The Predicate interface was introduced in Java 8 as a part of the java.util.function package. The release of version 8 marks the point at which Java adopted ample support for functional programming practices distending to include various new features, including lambda expressions, default methods, and predefined functional interfaces such as the Predicate itself.

Java is an object-oriented language, imperative in its essence (contrasting with the declarative practice that is functional programming). Nonetheless, it was possible to apply functional principles to Java programs prior to version 8, however it required additional work to bypass the innate structure of the language and resulted in convoluted code. Java 8 brought about ways to harness the true efficacy and ease to which functional programming aspires.

This guide will cover the use of Predicates as a form of Functional Interfaces in Java.

Note: It's highly recommended to get acquainted with Functional Interfaces and Lambda Expressions before proceeding to Predicates in Java.

Predicates in Java

A Functional Interface is an interface that has exactly one abstract method. It's typically a test() or apply() method and you test or apply some operation on an element.

For instance, we could try writing a personal "filtering" system that filters "friendly" people in a list, based on someone's personal preconceived notions.

Note: Our standard of "friendliness" will be set just for illustrational purposes, and doesn't reflect any real research or statistical analysis.

Assuming a Person has some hobbies and preferences:

enum PetPreference {
    DOGPERSON, CATPERSON, HASAPETSNAKE
}

public class Person {
    private String name;
    private int age;
    private boolean extrovert;
    private PetPreference petPreference;
    private List<String> hobbies;

    // Constructor, getters, setters and toString()
}

One could have a bias towards being friends with extroverts that have the same hobbies as they do. While this practice in real life probably isn't the best choice - we could filter a list of people based on their hobbies and other characteristics.

The functional interface's test() function will accept list of people to filter out, ending up with a group of people that are, according to the opinion applied, "nice people":

public interface Bias {
    boolean test(Person p);
}

Even though the Bias interface was written for this example, the general behavior it defines is implemented all the time in programming. We constantly apply logical tests to adjust the algorithm to the program's state.

The java.util.function package, employs Predicates to cover the cases where logical tests are to be applied, generically. In general, predicates are used to test something, and return a true or false value according to that test.

The predefined functional interface has the structure structure, albeit, accepts a generic parameter:

public interface Predicate<T> {
    boolean test(T t);
}

We can skip the creation of a custom Bias interface, and use a Predicate instead. It accepts an object to test and returns a boolean. That's what predicates do. Let's first import the function package:

import java.util.function.*;

We can test this out by creating a Person and testing them via a Predicate:

Person p1 = new Person("David", 35, true, PetPreference.DOGPERSON, "neuroscience", "languages", "travelling", "reading");

Predicate<Person> bias = p -> p.isExtrovert();
boolean result = bias.test(p1);
System.out.println(result);

The body of the test itself is defined in the Lambda Expression - we're testing whether a person's isExtrovert() field is true or false. This could be replaced with other operations, such as:

p -> p.getHobbies().contains("Being nice to people"); 

As long as the end result is a boolean - the body can represent any test. Now, let's define a filter() method that takes in a list of people and a predicate to use to filter them:

public static List<Person> filter(List<Person> people, Predicate<Person> bias) {
    List<Person> filteredPeople = new ArrayList<>();
    for (Person p : people) {
      if (bias.test(p)) {
        filteredPeople.add(p);
      }
    }
    return filteredPeople;
}

For each person in the list, we apply the test() method - and based on the result, add them or skip them in the filteredPeople list. Let's make a list of people and test the method out:

Person p1 = new Person("David", 35, true, PetPreference.DOGPERSON, "neuroscience", "languages", "travelling", "reading");
Person p2 = new Person("Marry", 35, true, PetPreference.CATPERSON, "archery", "neurology");
Person p3 = new Person("Jane", 15, false, PetPreference.DOGPERSON, "neurology", "anatomy", "biology");
Person p4 = new Person("Mariah", 27, true, PetPreference.HASAPETSNAKE, "hiking");
Person p5 = new Person("Kevin", 55, false, PetPreference.CATPERSON, "traveling", "swimming", "weightlifting");

List<Person> people = Arrays.asList(p1, p2, p3, p4, p5);

System.out.println(filter(people, p -> p.isExtrovert()));

Since a Predicate is a Functional Interface - we can use a Lambda Expression to define it's body anonymously in the method call.

This code results in:

[
Person{name='David', age=35, extrovert=true, petPreference=DOGPERSON, hobbies=[neuroscience, languages, travelling, reading]}, 
Person{name='Marry', age=35, extrovert=true, petPreference=CATPERSON, hobbies=[archery, neurology]}, 
Person{name='Mariah', age=27, extrovert=true, petPreference=HASAPETSNAKE, hobbies=[hiking]}
]

The test() method

We can inject different behaviors to the Predicate's test() method via lambdas and execute it against Person objects:

Person randomPerson = new Person("Aaron", 41, true, PetPreference.DOGPERSON, "weightlifting", "kinesiology");

Predicate<Person> sociable =  c -> c.isExtrovert() == true;
System.out.println(sociable.test(randomPerson));

Predicate<Person> dogPerson = c -> c.getPetPreference().equals(PetPreference.DOGPERSON);
System.out.println(dogPerson.test(randomPerson));

Predicate<Person> seniorCitizen = c -> c.getAge() > 65;
System.out.println(seniorCitizen.test(randomPerson));

The sociable predicate alters the innate test()method to select extroverts. The dogPerson predicate tests to see if a person is a dog person and seniorCitizen predicate returns true for people over the age of 65.

Aaron (randomPerson) is an extrovert, a dog person, and he still has a few good years until he becomes a senior citizen. The console should read:

true
true
false

We have compared Aaron's characteristics against some fixed values (true, DOGPERSON, 65) but what if we wanted to generalize these tests?

We could create a method to identify several age scopes rather than just senior citizens or we could have a pet preference method that is parameterized. In these cases, we need additional arguments to work with and since the Predicates are only meant to operate on one object of a spesific type, we have to build a method around them.

Let's create a method that would take a list of hobbies and compare them against the hobbies belonging to the Person in question:

public static Predicate<Person> hobbyMatch(String ... hobbies) {
    List<String> hobbiesList = Arrays.asList(hobbies);
    return (c) -> {
        List<String> sharedInterests = new ArrayList<>(hobbiesList);
        sharedInterests.retainAll(c.getHobbies());
        return sharedInterests.size() > 0;
    };
}

The hobbyMatch() method takes a variable-length list of Strings and parses them into a list. The lambda that hobbyMatch() returns duplicates this list in the form of an ArrayList and applies the built-in retainAll() method on the duplicate striping off the elements that don't match any elements of the c.getHobbies() (retaining the common elements among two lists).

Note: We have copied hobbiesList to sharedInterests since lambdas are pure functions, and they are not to cause any side effects (such as altering a global variable).

After filtering the sharedInterest list, the lambda expression checks whether there exists more than one item in the list and returns true if that is the case.

We can pass hobbyMatch() to the filter() method along with a group of people and list them out on the console:

Person p1 = new Person("Marshall", 35, true, PetPreference.DOGPERSON, "basketball", "eating", "reading");
Person p2 = new Person("Marry", 35, true, PetPreference.CATPERSON, "archery", "swimming");
Person p3 = new Person("Jane", 15, false, PetPreference.DOGPERSON, "neurology", "anatomy", "biology");
Person p4 = new Person("Mariah", 27, true, PetPreference.HASAPETSNAKE, "hiking");
Person p5 = new Person("Kevin", 55, false, PetPreference.CATPERSON, "traveling", "swimming", "weightlifting");

List<Person> people = Arrays.asList(p1, p2, p3, p4, p5);

System.out.println(filter(people, hobbyMatch("neurology", "weightlifting")));

This results in:

[
Person{name='Jane', age=15, extrovert=false, petPreference=DOGPERSON, hobbies=[neurology, anatomy, biology]}, 
Person{name='Kevin', age=55, extrovert=false, petPreference=CATPERSON, hobbies=[traveling, swimming, weightlifting]}
]

Static Method: isEqual()

Along with the Predicate interface came a set of helper methods to aid in logical operations. isEqual() is a static method that compares two objects via the equals() method of the Predicate object’s type parameter:

Free eBook: Git Essentials

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

Predicate<Integer> equalToThree = Predicate.isEqual(3);
System.out.println(equalToThree.test(5));

Predicate<String> equalToAaron = Predicate.isEqual("Aaron");
System.out.println(equalToAaron.test(randomPerson.getName()));

The equalToThree predicate is programmed to compare its argument to 3 via the Integer object's equal() method. equalToThree.test(5) will fail miserably.

equalToAaron will use the String object's equal() method to test whether the argument to its test() method equals "Aaron".

If we apply the test to previously created randomPerson, the method returns true.

Default Methods and Predicate Chaining

The Predicate interface has three default methods that aid in the creation of complex logical expressions. The default methods and(), or() and negate() take in a lambda expression and return a new Predicate object with the defined behavior. When linked together in a chain, each new Predicate resulting from the default method operates on the preceding link.

Each chain should have the functional method test() as its final link, whose parameter gets fed into the first Predicate to start off the chain.

and()

We use the default and() method to apply the logical and operation (&&) on two predicates.

Person randomPerson = new Person("Aaron", 41, true, PetPreference.DOGPERSON, "weightlifting", "kinesiology");

Predicate<Person> dogPerson = c -> c.getPetPreference().equals(PetPreference.DOGPERSON);

Predicate<Person> sociable =  c -> c.isExtrovert() == true;
System.out.println(sociable.test(randomPerson));

Predicate<Person> seniorCitizen = c -> c.getAge() > 65;

Now, we can chain these predicates:

// Chaining with anonymous predicate
System.out.println(dogPerson.and(c -> c.getName().equals("David")).test(randomPerson));
// Chaining with existing predicate
System.out.println(seniorCitizen.and(dogPerson).test(randomPerson));

We have brought back Aaron the randomPerson to feed into our logical chains, and the dogPerson, sociable and seniorCitizen predicates to be a link in them.

Let's look at the first composed predicate of the program:

dogPerson.and(c -> c.getName().equals("David")).test(randomPerson)

randomPerson first goes through the test of the dogPerson predicate. Since Aaron is indeed a dog person, program moves on to the next link to apply its test. The and() method creates a new Predicate whose functional test() method is defined by the lambda expression given. Since "Aaron" is not equal to "David", the test fails, and the chain returns false.

In the second chain, we have created links between the seniorCitizen and dogPerson tests. Since the first test to be applied is of seniorCitizen and Aaron is not yet 65, the first link returns false, and the system gets short-circuited. The chain returns false without the need to evaluate the dogPerson predicate.

or()

We can connect two predicates via or() to perform logical or operation (||). Let's create a new list of people with a couple of hobbies, inspired by a popular movie's character cast:

Person jo = new Person("Josephine", 21, true, PetPreference.DOGPERSON, "writing", "reading");
Person meg = new Person("Margaret", 23, true, PetPreference.CATPERSON, "shopping", "reading");
Person beth = new Person("Elizabeth", 19, false, PetPreference.DOGPERSON, "playing piano", "reading");
Person amy = new Person("Amy", 17, true, PetPreference.CATPERSON, "painting");

Now, let's use the filter() method to extract the people from this list that like reading or are sociable:

List<Person> lilWomen = Arrays.asList(jo, meg, beth, amy);
List<Person> extrovertOrReader = filter(lilWomen, hobbyMatch("reading").or(sociable));
System.out.println(extrovertOrReader);

This results in:

[
Person{name='Josephine', age=21, extrovert=true, petPreference=DOGPERSON, hobbies=[writing, reading]}, 
Person{name='Margaret', age=23, extrovert=true, petPreference=CATPERSON, hobbies=[shopping, reading]}, 
Person{name='Elizabeth', age=19, extrovert=false, petPreference=DOGPERSON, hobbies=[playing piano, reading]}, 
Person{name='Amy', age=17, extrovert=true, petPreference=CATPERSON, hobbies=[painting]}
]
negate()

The negate()method reverses the result of the predicate that it applies to:

sociable.negate().test(jo);

This statement tests jo for sociability. Then negate() applies to the result of sociable.test() and reverses it. Since jo is indeed sociable, the statement results in false.

We can use sociable.negate() call in the filter() method to search for introverted little women and add on .or(hobbyMatch("painting")) to include in the painters:

List<Person> shyOrPainter = filter(lilWomen, sociable.negate().or(hobbyMatch("painting")));
System.out.println(shyOrPainter);

This piece of code results in:

[
Person{name='Elizabeth', age=19, extrovert=false, petPreference=DOGPERSON, hobbies=[playing piano, reading]}, 
Person{name='Amy', age=17, extrovert=true, petPreference=CATPERSON, hobbies=[painting]}
]
not()

not() is a static method that works in the same way negate() does. While negate() operates on an existing predicate, static not() method is supplied a lambda expression or an existing predicate via which it creates a new predicate with reversed calculation:

Boolean isJoIntroverted = sociable.negate().test(jo);
Boolean isSheTho = Predicate.not(sociable).test(jo);
Predicate<Person> withALambda = Predicate.not(c -> c.isExtrovert());
Boolean seemsNot = withALambda.test(jo);

System.out.println("Is Jo an introvert? " + isJoIntroverted + " " + isSheTho + " " + seemsNot);

Although all three booleans created by the above program carry the same information (Jo is not an introvert), they go about collecting the information in different ways.

Notice that we did not assign Predicate.not(c -> c.isExtrovert()).test(jo) directly to the seemsNot boolean. We had to first declare a Predicate of type Person and reap the result of its test() method later.

If we try to execute the assignment statement:

Boolean seemsNot = Predicate.not(c -> c.isExtrovert()).test(jo)

The compiler screams in horror. It has no way of knowing what the c in the lambda stands for or whether c is even capable of executing isExtrovert().

Predicate Subtypes

There exist three Predicate subtypes to serve non-generic objects. The IntPredicate, LongPredicate and DoublePredicate operate on Integers, Longs and Doubles, respectively. They define the default methods of the generic Predicate, yet these methods are targeted to, well, Integers, Longs and Doubles.

The isEqual() method does not apply to these subtypes simply because the operation can easily be achieved via the use of == operator:

IntPredicate intPredicate = c -> c <= 5;
LongPredicate longPredicate = c -> c%2 == 0;
DoublePredicate doublePredicate = c -> c > 6.0;

System.out.println(intPredicate.negate().test(2));
System.out.println(longPredicate.test(10L));
System.out.println(doublePredicate.or(c -> c < 11.0).test(7.1));

This results in:

false
true
true
Binary Predicate

Binary predicates operate on two objects (they can be of the same type or they can be instants of different classes) rather than one, and are represented by the BiPredicate interface.

We can create a binary predicate to check if the two Person objects have any shared hobbies, for instance:

BiPredicate<Person, Person> sharesHobbies = (x, y) -> {
	List<String> sharedInterests = new ArrayList<>(x.getHobbies());
    sharedInterests.retainAll(y.getHobbies());
    return sharedInterests.size() > 0;
};

Person x = new Person("Albert", 29, true, PetPreference.DOGPERSON, "football", "existentialism");
Person y = new Person("Jean-Paul", 37, false, PetPreference.CATPERSON, "existentialism");

System.out.println(sharesHobbies.test(x,y));

The binary predicate sharesHobbies works in the same way as the previously created hobbyMatch() method, though sharesHobbies compares the hobbies of two Persons instead of comparing the hobbies of one Person to a given list of hobbies.

The code results in:

true

Conclusion

The Predicate interface was introduced in Java 8 as a part of the java.util.function package. The release of version 8 marks the point at which Java adopted ample support for functional programming practices distending to include various new features, including lambda expressions, default methods, and predefined functional interfaces such as the Predicate itself.

Using Predicates doesn't necessarily require the full scope of understanding of Functional Programming - but it nevertheless introduces OOP developers to several very useful and flexible concepts.

We've focused on Predicates, one type of Functional Interfaces in Java, showcasing how they can be used in filtration systems to represent search criteria.

Last Updated: December 4th, 2021
Was this article helpful?

Improve your dev skills!

Get tutorials, guides, and dev jobs in your inbox.

No spam ever. Unsubscribe at any time. Read our Privacy Policy.

Want a remote job?

    Prepping for an interview?

    • Improve your skills by solving one coding problem every day
    • Get the solutions the next morning via email
    • Practice on actual problems asked by top companies, like:
     
     
     

    Make Clarity from Data - Quickly Learn Data Visualization with Python

    Learn the landscape of Data Visualization tools in Python - work with Seaborn, Plotly, and Bokeh, and excel in Matplotlib!

    From simple plot types to ridge plots, surface plots and spectrograms - understand your data and learn to draw conclusions from it.

    © 2013-2021 Stack Abuse. All rights reserved.