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", "traveling", "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", "traveling", "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 its body anonymously in the method call.
This code results in:
[
Person{name='David', age=35, extrovert=true, petPreference=DOGPERSON, hobbies=[neuroscience, languages, traveling, 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 the 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 specific 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:
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!
[
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:
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, the 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 Person
s 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 Predicate
s 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.