This content originally appeared on DEV Community and was authored by André Borba
Introduction
Welcome to the first practical episode of Surfing with FP Java. Today, we’ll explore Predicate, the functional interface that introduces declarative boolean logic in Java.
At first glance, Predicate
seems simple, it takes a value and returns true
or false
. But its impact is profound: it allows us to externalize conditions, compose business rules, reduce coupling, and write code that is reusable and testable.
What Is Predicate?
In its pure form:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
// Default methods for composition
default Predicate<T> and(Predicate<? super T> other) { ... }
default Predicate<T> or(Predicate<? super T> other) { ... }
default Predicate<T> negate() { ... }
// Handy static method
static <T> Predicate<T> isEqual(Object targetRef) { ... }
}
- Input: an object of type T.
- Output: a boolean indicating if the condition is satisfied.
Why Use Predicate?
Traditionally, conditions in Java were written in an imperative style:
if (user.getAge() > 18 && user.isActive()) {
// allowed
}
With Predicate
, we transform this into a first-class function. This means we can pass, compose, and reuse the logic more elegantly:
Predicate<User> isAdult = user -> user.getAge() > 18;
Predicate<User> isActive = User::isActive;
Predicate<User> canAccess = isAdult.and(isActive);
Now canAccess is a reusable and composable rule, decoupled from the control flow.
Practical Examples
1. Filtering Collections
One of the most common uses is with Streams:
List<String> names = List.of("Andre", "Borba", "John", "Amanda", "Vlad");
Predicate<String> startsWithA = name -> name.startsWith("A");
List<String> filtered = names.stream()
.filter(startsWithA)
.toList();
System.out.println(filtered); // [Andre, Amanda]
2. Composing Conditions
Predicates let us create declarative business rules:
Predicate<User> isAdult = u -> u.getAge() >= 18;
Predicate<User> isActive = User::isActive;
Predicate<User> canAccess = isAdult.and(isActive);
List<User> usersAllowed = users.stream()
.filter(canAccess)
.toList();
This separates business rules from imperative control, improving clarity.
3. Negating Conditions
With negate(), you can easily invert logic:
Predicate<String> empty = String::isEmpty;
Predicate<String> notEmpty = empty.negate();
List<String> nonEmptyNames = names.stream()
.filter(notEmpty)
.toList();
4. Using isEqual
Predicate also provides a handy equality check:
Predicate<String> isEqualToAdmin = Predicate.isEqual("admin");
System.out.println(isEqualToAdmin.test("admin")); // true
System.out.println(isEqualToAdmin.test("user")); // false
Real-World Patterns
Input Validation
Compose multiple predicates to validate DTOs or commands before persistence.Feature Flags
Represent feature enablement logic declaratively.Dynamic Filters
Build predicates at runtime to support dynamic API filters.
Best Practices
Name Predicates Clearly
Use domain-oriented names like isEligible or hasPermission instead of p1 or p2.Prefer Composition Over Nesting
Combine with and, or, negate rather than writing nested if blocks inside lambdas.Domain Isolation
Store reusable predicates as constants in utility classes or within domain services.
Common Pitfalls
Overuse: Not every condition needs to be extracted into a predicate.
Complexity: Too many chained predicates can hurt readability. Consider specialized classes if logic becomes too complex.
Performance: Each predicate in a chain is executed sequentially. Watch out in large pipelines.
Functional Analogy
Think of Predicate as the gatekeeper in your code:
- It receives someone (T).
- Decides if they are allowed in (boolean).
- Can work alone (test) or cooperate with other gatekeepers (and, or).
Conclusion
Predicate is much more than a simple boolean function, it is the building block of declarative logic in Java. By mastering it, you can externalize rules, compose conditions, and write code that is more expressive, reusable, and clean.
What’s Next
In the next episode, we’ll explore Function<T, R>
, the “transformer” of data.
Where Predicate answers “Is this valid?”, Function answers “How do I transform this into something else?”.
From here, functional composition becomes even more powerful.
This content originally appeared on DEV Community and was authored by André Borba