Java Predicate: Syntax and Practical Usage
java predicate: Learn how to use Java Predicate for filtering collections, combining conditions, and writing cleaner lambda-based logic.
java predicate requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
The Predicate<T> interface in Java is a functional interface that accepts one argument and returns a boolean. It is part of the java.util.function package and serves as the foundation for many stream operations, particularly filter(). Because it is a functional interface, you can supply a predicate as a lambda expression or method reference, which makes collection filtering concise and readable.
The Predicate Interface and Its Signature
The core of Predicate<T> is the abstract method boolean test(T t). This method evaluates the condition and returns true or false. The interface also provides default methods for logical composition: and(), or(), and negate(). These default methods allow you to build complex conditions without writing verbose if-else chains.
Predicate<String> isEmpty = String::isEmpty; boolean result = isEmpty.test(""); // true
The test method is the only abstract method, so any lambda or method reference that matches the signature (T) -> boolean can be used as a predicate. This is what makes predicates so flexible in Java 8 and later.
Writing Predicates with Lambda Expressions
Lambda expressions are the most common way to create a predicate. The syntax is straightforward: the left side is the parameter, and the right side is the boolean expression. For example, to check if an integer is even:
Predicate<Integer> isEven = n -> n % 2 == 0;
You can use predicates to encapsulate conditions that would otherwise be scattered across your code. Instead of writing a loop with an if statement, you can define the condition once and reuse it. This becomes especially useful when the same condition appears in multiple places.
Predicate<String> hasMinLength = s -> s != null && s.length() >= 5;
Note that the lambda body can include multiple statements if you use braces, but for simple boolean expressions, a single expression is idiomatic.
Combining Predicates with and, or, negate
The default methods and, or, and negate allow you to compose predicates. The and method returns a predicate that is true only when both predicates are true. The or method returns true when at least one is true. The negate method reverses the result.
Predicate<Integer> isEven = n -> n % 2 == 0; Predicate<Integer> isPositive = n -> n > 0; Predicate<Integer> isEvenAndPositive = isEven.and(isPositive); Predicate<Integer> isEvenOrPositive = isEven.or(isPositive); Predicate<Integer> isOdd = isEven.negate();
These compositions are evaluated with short-circuiting. In and, if the first predicate returns false, the second is not evaluated. In or, if the first returns true, the second is skipped. This behavior is consistent with the logical operators && and ||.
Using Predicate with Stream.filter
The most common use of Predicate is with the Stream.filter() method. filter takes a predicate and returns a stream containing only the elements that match. This allows you to express filtering logic declaratively.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); List<String> longNames = names.stream() .filter(name -> name.length() > 4) .collect(Collectors.toList());
You can also reuse a predicate across multiple streams. Instead of duplicating the condition, define it once and pass it to each filter call. This reduces duplication and makes the code easier to maintain.
Predicate<String> isLongName = name -> name.length() > 4; List<String> filtered = names.stream().filter(isLongName).collect(Collectors.toList());
Predicate and Method References
Method references are a compact alternative to lambdas when the condition already exists as a method. For example, String::isEmpty is a method reference that matches Predicate<String>. You can also reference instance methods on a particular object or static methods.
Predicate<String> isEmpty = String::isEmpty; Predicate<String> isNotEmpty = isEmpty.negate();
Method references make the intent clearer when the method name is descriptive. However, they only work when the method signature matches the predicate's test method: it must take one argument and return a boolean. For instance, this::isValid works if isValid is an instance method with that signature.
Handling Null and Edge Cases
A predicate's test method will throw a NullPointerException if it dereferences a null argument without a null check. This is a common source of bugs when filtering collections that may contain nulls. To handle nulls safely, you can use Objects::nonNull as a predicate or include a null check in your lambda.
Predicate<String> safeLength = s -> s != null && s.length() > 3;
Alternatively, use Objects.isNull and Objects.nonNull from the standard library. These methods are useful when you need to filter out null values before applying other conditions.
List<String> withNulls = Arrays.asList("a", null, "bb", null, "ccc"); List<String> nonNull = withNulls.stream() .filter(Objects::nonNull) .collect(Collectors.toList());
Remember that and, or, and negate are also methods on the predicate, so they do not perform null checks on the argument themselves. The null check must be part of the predicate logic.
Performance and Runtime Considerations
Predicates are lightweight objects, but composing them with and, or, and negate creates new predicate instances. In a hot loop, this allocation overhead is usually negligible, but it is worth being aware of if you are building a pipeline that processes millions of elements. The JIT compiler often inlines these small functional interfaces, so the cost is rarely a bottleneck.
A more significant consideration is the cost of the predicate logic itself. If your predicate performs expensive computation, such as a database query or a complex regular expression, that cost dominates. Reusing a precompiled Pattern or caching results can have a much larger impact than the predicate abstraction.
When working with parallel streams, predicates must be stateless and thread-safe. If a predicate maintains mutable state, it can cause incorrect results or race conditions. The standard approach is to keep predicates pure: they should not modify external state and should depend only on their argument.
Choosing Between Predicate and Custom Condition
While Predicate is the idiomatic choice for stream filtering, it is not always the right tool. If you need to perform additional operations inside the condition—such as logging, counting, or throwing a custom exception—a lambda expression with a block body may be clearer. For example, a predicate that logs every evaluation might be better expressed as a separate method that returns a boolean and logs internally.
Another case is when you need to pass additional parameters to the condition. A predicate only accepts the element itself. If the condition depends on an external value that changes frequently, you can capture that value in the lambda, but be aware that the predicate is then stateful. If the external value changes between stream operations, the predicate may not behave as expected.
In such situations, you can fall back to a traditional loop or a custom functional interface with more parameters. The Predicate interface is intentionally simple; it trades flexibility for consistency and readability. Use it when the condition is a pure function of the element, and choose a more explicit approach when the logic is more involved.