In this detailed guide, you will learn about Java Predicates, a new and useful feature in Java 8. This article explains what Java Predicates are and how to use them in various ways.
It will help you with everything you need to know about filtering data, chaining predicates, and avoiding common mistakes. With Java Predicates, you can elevate your Java programming skills and take your applications to the next level.
What is Java Predicates
In Java 8, a predicate is a functional interface introduced in the java.util.function
package. It represents a boolean-valued function that takes an argument and returns true or false. The Predicate
interface has a single abstract method called test()
which is used to evaluate the boolean condition.
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
Usually predicates are useful tools for filtering or evaluating elements in collections.
Getting Started
A Java Predicate is a functional interface that can be defined using a lambda expression. Here’s a simple example of a Predicate that checks if a string has an even length:
Predicate<String> startWithLetterN = s -> s.startsWith("N");
In this code, startWithLetterN
is a Predicate that takes a string s
and checks if it's starts with the letter N
. We use the lambda expression s -> s.startsWith("N")
to define this logic.
Once we have our Predicate, we can use it to evaluate data using the test()
method.
Boolean result = startWithLetterN.test("Mateo");
@Test
void verify() {
assertThat(result).isFalse();
}
Chaining Predicates
Applying chaining operations on predicates in pretty simple, and can be achieved using the method and
on the first predicate and passing the second as a parameter.
Predicate<String> startWithLetterP = s -> s.startsWith("P");
Predicate<String> lengthGreaterThan5 = s -> s.length() > 5;
Predicate<String> startWithPAndGreaterThan5 = startWithLetterP.and(lengthGreaterThan5);
@Test
void check() {
assertThat(startWithPAndGreaterThan5.test("Penelope")).isTrue();
}
Filtering Data with Predicates
Predicates are often used to filter data. For instance, you can use them to filter a collection of items that match a certain condition. Here’s an example of using a Predicate to filter a list of numbers:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Predicate<Integer> isGreaterThan4 = n -> n > 4;
Predicate<Integer> isEven = n -> n % 2 == 0;
List<Integer> matchingNumbers = numbers.stream()
.filter(isEven.and(isGreaterThan4))
.collect(Collectors.toList());
@Test
void check() {
assertThat(matchingNumbers).hasSize(3).containsExactly(6, 8, 10);
}
You can use different predicates methods to match all your use cases.
For example, once we have a Predicate like
startWithLetterN
, we can use it in multiple places throughout our code. This is not possible with the conditions defined inside a loop or a Stream’s filter()
method.In summary, while loops and Streams can be used to filter data in Java, Predicates offer a more flexible and reusable approach, especially for complex conditions and larger applications.
Pitfalls to Avoid
There are potential pitfalls when using Java predicates. The most common one is the NullPointerExceptions
with Method References. That usually happens when you’re dealing with collections that contain null
values.
List<String> categories = List.of("Culture", "Entertainment", null);
Predicate<String> isShortWord = s -> s.length() < 4;
@Test
void check() {
categories.stream()
.filter(isShortWord)
.forEach(System.out::println);
}
// This will throw a java.lang.NullPointerException 🪲
In this example, we’re trying to filter a list of strings to only retrieve those who have short words. However, our list contains a null
value, which causes a NullPointerException
when we try to call the length()
method on it.
To avoid this issue, you can add another check to your Predicate to ignore null
values:
List<String> categories = Arrays.asList("Culture", "Entertainment", null);
Predicate<String> isShortWord = s -> Objects.nonNull(s) && s.length() < 4;
@Test
void check() {
categories.stream()
.filter(isShortWord)
.forEach(System.out::println);
}
// This will return an empty array []
Another pitfall to avoid is the order of chaining operations with predicate. When chaining Predicates using and()
, or()
, and negate()
, keep in mind that the order of operations can affect the result.
For example, the and()
operation is not commutative when dealing with null
values:
Predicate<Integer> isNotNull = Objects::nonNull;
Predicate<Integer> isGreaterThan3 = s -> s > 3;
Predicate<Integer> correctOrder = isNotNull.and(isGreaterThan3);
Predicate<Integer> incorrectOrder = isGreaterThan3.and(isNotNull);
@Test
void check() {
System.out.println(correctOrder.test(null)); // false
System.out.println(incorrectOrder.test(null)); // NullPointerException
}
I hope you enjoyed reading this, and I'm curious to hear if this tutorial helped you. Please let me know your thoughts below in the comments. Don't forget to subscribe to my newsletter to avoid missing my upcoming blog posts.
You can also find me here LinkedIn • Twitter • GitHub or Medium
End
This article serves as your comprehensive roadmap to mastering Java Predicates. You've learned how they work, how important they are in Java programming, and how to use them well in your code.
By reading this guide, you've learned about Java Predicates, learned some advanced techniques and can use them in your projects with confidence and finesse.