- Understand that Predicate is a functional interface that tests a condition and returns true or false
- Learn to use Predicate with lambda expressions for filtering operations
- Apply Predicate methods like
test(),and(),or(), andnegate() - Use Predicates with Stream API operations like
filter()
Functional programming transformed Java when Java 8 introduced lambda expressions and the java.util.function package.
Among the functional interfaces in this package, Predicate<T> stands out as one of the most practical and frequently
used. At its core, a Predicate answers a simple question: given some input, does it satisfy a particular condition?
Every time you filter a collection, validate user input, or implement conditional logic, you're performing predicate
operations whether you realize it or not. The Predicate interface formalizes this pattern, giving you a type-safe,
composable way to express boolean conditions. What makes Predicate particularly powerful isn't just that it encapsulates
true/false logic—it's that predicates compose elegantly, allowing you to build complex conditions from simple building
blocks.
This matters because real applications rarely deal with single, isolated conditions. You need to check if a user is both authenticated and authorized, or whether a transaction meets one set of criteria or another. Predicate gives you the tools to express these compound conditions clearly and maintainably, and integrates seamlessly with the Stream API that drives modern Java collection processing.
A Predicate is a functional interface with a single abstract method that takes one argument and returns a boolean. The interface definition looks like this:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
// default methods: and(), or(), negate()
// static method: isEqual()
}Because Predicate has exactly one abstract method (test), it qualifies as a functional interface, which means you can
implement it using lambda expressions or method references. The generic type parameter T represents the type of input
the predicate evaluates. When you create a Predicate<String>, you're defining a condition that operates on strings.
A Predicate<Integer> works with integers, and so on.
The test() method performs the actual evaluation. You pass in a value of type T, and the predicate returns true if
the value satisfies the condition, false otherwise. This simple contract makes predicates incredibly versatile—any
boolean condition you can express can become a predicate.
Lambda expressions provide the cleanest syntax for creating predicates. The pattern follows the
form (parameter) -> condition, where the condition must evaluate to a boolean value.
package blog.academy.javapro;
import java.util.function.Predicate;
public class PredicateBasics {
public static void main(String[] args) {
Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<String> isEmpty = s -> s.isEmpty();
Predicate<String> startsWithA = s -> s.startsWith("A");
System.out.println("Is 5 positive? " + isPositive.test(5));
System.out.println("Is -3 positive? " + isPositive.test(-3));
System.out.println("Is 4 even? " + isEven.test(4));
System.out.println("Is 7 even? " + isEven.test(7));
System.out.println("Does 'Alice' start with A? " + startsWithA.test("Alice"));
System.out.println("Does 'Bob' start with A? " + startsWithA.test("Bob"));
}
}Notice how each predicate captures a single, focused condition. This approach follows the Unix philosophy: do one thing
well. The isPositive predicate doesn't concern itself with whether a number is even or prime—it simply checks if it's
greater than zero. This single-responsibility design makes predicates easier to test, understand, and reuse across your
codebase.
You can also create predicates using method references when an existing method matches the predicate signature. If you have a method that takes a single parameter and returns a boolean, you can reference it directly:
package blog.academy.javapro;
import java.util.function.Predicate;
public class PredicateMethodReference {
private static boolean isLongString(String s) {
return s.length() > 10;
}
private static boolean isPrime(int n) {
if (n < 2) return false;
for (int i = 2; i <= Math.sqrt(n); i++) {
if (n % i == 0) return false;
}
return true;
}
public static void main(String[] args) {
Predicate<String> isLong = PredicateMethodReference::isLongString;
Predicate<Integer> checkPrime = PredicateMethodReference::isPrime;
System.out.println("Is 'hello' long? " + isLong.test("hello"));
System.out.println("Is 'extraordinary' long? " + isLong.test("extraordinary"));
System.out.println("Is 17 prime? " + checkPrime.test(17));
System.out.println("Is 20 prime? " + checkPrime.test(20));
}
}The real power of predicates emerges when you combine them. The Predicate interface provides three default methods for
composition: and(), or(), and negate(). These methods create new predicates from existing ones, following boolean
logic.
The and() method combines two predicates with a logical AND operation. The resulting predicate returns true only if
both constituent predicates return true. Think of it as a stricter filter that applies multiple requirements:
package blog.academy.javapro;
import java.util.function.Predicate;
public class PredicateComposition {
public static void main(String[] args) {
Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> isLessThan100 = n -> n < 100;
Predicate<Integer> isPositiveEven = isPositive.and(isEven);
Predicate<Integer> isValidRange = isPositive.and(isLessThan100);
System.out.println("Is 4 positive and even? " + isPositiveEven.test(4));
System.out.println("Is 7 positive and even? " + isPositiveEven.test(7));
System.out.println("Is -2 positive and even? " + isPositiveEven.test(-2));
System.out.println("Is 50 in valid range (0-100)? " + isValidRange.test(50));
System.out.println("Is 150 in valid range? " + isValidRange.test(150));
System.out.println("Is -10 in valid range? " + isValidRange.test(-10));
}
}The or() method combines predicates with logical OR. The resulting predicate returns true if either predicate returns
true. This creates a more permissive filter that accepts values meeting any of several criteria:
package blog.academy.javapro;
import java.util.function.Predicate;
public class PredicateOr {
public static void main(String[] args) {
Predicate<String> startsWithA = s -> s.startsWith("A");
Predicate<String> endsWithZ = s -> s.endsWith("z");
Predicate<String> isShort = s -> s.length() < 4;
Predicate<String> specialName = startsWithA.or(endsWithZ);
Predicate<String> interesting = startsWithA.or(isShort);
System.out.println("Alice (starts with A or ends with z): " + specialName.test("Alice"));
System.out.println("buzz (starts with A or ends with z): " + specialName.test("buzz"));
System.out.println("Charlie (starts with A or ends with z): " + specialName.test("Charlie"));
System.out.println("Amy (starts with A or short): " + interesting.test("Amy"));
System.out.println("Bob (starts with A or short): " + interesting.test("Bob"));
System.out.println("Christopher (starts with A or short): " + interesting.test("Christopher"));
}
}The negate() method creates the logical opposite of a predicate. If the original predicate returns true, the negated
version returns false, and vice versa. This proves useful when you want to invert an existing condition rather than
writing a new one:
package blog.academy.javapro;
import java.util.function.Predicate;
public class PredicateNegate {
public static void main(String[] args) {
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> isOdd = isEven.negate();
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();
System.out.println("Is 8 odd? " + isOdd.test(8));
System.out.println("Is 9 odd? " + isOdd.test(9));
System.out.println("Is 'hello' not empty? " + isNotEmpty.test("hello"));
System.out.println("Is '' not empty? " + isNotEmpty.test(""));
}
}You can chain these composition methods to build complex conditions from simple predicates. The key insight here is that each composition method returns a new Predicate, so you can continue chaining operations:
package blog.academy.javapro;
import java.util.function.Predicate;
public class PredicateChaining {
public static void main(String[] args) {
Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> isLarge = n -> n > 1000;
Predicate<Integer> complexCondition = isPositive
.and(isEven)
.and(isLarge.negate());
System.out.println("Testing 500: " + complexCondition.test(500));
System.out.println("Testing 501: " + complexCondition.test(501));
System.out.println("Testing 1500: " + complexCondition.test(1500));
System.out.println("Testing -10: " + complexCondition.test(-10));
}
}This predicate accepts numbers that are positive, even, and not large (less than or equal to 1000). The composition reads almost like natural language once you understand the pattern, and each constituent predicate remains reusable in other contexts.
Predicates integrate seamlessly with Java's Stream API, particularly in filtering operations. The filter() method
accepts a predicate and creates a new stream containing only elements that satisfy the condition:
package blog.academy.javapro;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class PredicateWithStreams {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> isGreaterThan5 = n -> n > 5;
List<Integer> evenNumbers = numbers.stream()
.filter(isEven)
.collect(Collectors.toList());
List<Integer> evenAndLarge = numbers.stream()
.filter(isEven.and(isGreaterThan5))
.collect(Collectors.toList());
System.out.println("Even numbers: " + evenNumbers);
System.out.println("Even numbers greater than 5: " + evenAndLarge);
}
}The pattern extends naturally to more complex data types. When filtering collections of objects, predicates let you express business rules clearly:
package blog.academy.javapro;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class PredicateObjectFiltering {
static class Employee {
private String name;
private int age;
private double salary;
public Employee(String name, int age, double salary) {
this.name = name;
this.age = age;
this.salary = salary;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public double getSalary() {
return salary;
}
@Override
public String toString() {
return String.format("%s (age %d, $%.0f)", name, age, salary);
}
}
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("Alice", 28, 75000),
new Employee("Bob", 35, 85000),
new Employee("Charlie", 42, 95000),
new Employee("Diana", 31, 80000),
new Employee("Eve", 26, 70000)
);
Predicate<Employee> isYoung = e -> e.getAge() < 30;
Predicate<Employee> isHighEarner = e -> e.getSalary() > 80000;
Predicate<Employee> isExperienced = e -> e.getAge() >= 35;
List<Employee> youngEmployees = employees.stream()
.filter(isYoung)
.collect(Collectors.toList());
List<Employee> seniorHighEarners = employees.stream()
.filter(isExperienced.and(isHighEarner))
.collect(Collectors.toList());
List<Employee> targetHires = employees.stream()
.filter(isYoung.or(isHighEarner))
.collect(Collectors.toList());
System.out.println("Young employees: " + youngEmployees);
System.out.println("Senior high earners: " + seniorHighEarners);
System.out.println("Young or high earners: " + targetHires);
}
}You can also use predicates with other stream operations beyond filter(). The anyMatch(), allMatch(),
and noneMatch() terminal operations accept predicates to check whether stream elements satisfy conditions:
package blog.academy.javapro;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
public class PredicateMatching {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(2, 4, 6, 8, 10);
List<Integer> mixed = Arrays.asList(1, 2, 3, 4, 5);
Predicate<Integer> isEven = n -> n % 2 == 0;
boolean allEven = numbers.stream().allMatch(isEven);
boolean someEven = mixed.stream().anyMatch(isEven);
boolean noneOdd = numbers.stream().noneMatch(isEven.negate());
System.out.println("Are all numbers even? " + allEven);
System.out.println("Are some mixed numbers even? " + someEven);
System.out.println("Are none of the numbers odd? " + noneOdd);
List<String> names = Arrays.asList("Alice", "Andrew", "Anna", "Bob");
Predicate<String> startsWithA = s -> s.startsWith("A");
boolean allStartWithA = names.stream().allMatch(startsWithA);
boolean anyStartsWithA = names.stream().anyMatch(startsWithA);
System.out.println("Do all names start with A? " + allStartWithA);
System.out.println("Do any names start with A? " + anyStartsWithA);
}
}Predicates shine in validation scenarios where you need to enforce business rules. Rather than scattering conditional logic throughout your codebase, you can centralize validation rules as named predicates:
package blog.academy.javapro;
import java.util.function.Predicate;
public class ValidationExample {
static class User {
private String username;
private String email;
private int age;
public User(String username, String email, int age) {
this.username = username;
this.email = email;
this.age = age;
}
public String getUsername() {
return username;
}
public String getEmail() {
return email;
}
public int getAge() {
return age;
}
}
private static final Predicate<User> hasValidUsername =
u -> u.getUsername() != null && u.getUsername().length() >= 3;
private static final Predicate<User> hasValidEmail =
u -> u.getEmail() != null && u.getEmail().contains("@");
private static final Predicate<User> isAdult =
u -> u.getAge() >= 18;
private static final Predicate<User> isValidUser =
hasValidUsername.and(hasValidEmail).and(isAdult);
public static void main(String[] args) {
User validUser = new User("alice123", "alice@example.com", 25);
User invalidUser1 = new User("ab", "alice@example.com", 25);
User invalidUser2 = new User("bob123", "invalid-email", 20);
User minorUser = new User("charlie", "charlie@example.com", 16);
System.out.println("Valid user is valid: " + isValidUser.test(validUser));
System.out.println("Short username is valid: " + isValidUser.test(invalidUser1));
System.out.println("Invalid email is valid: " + isValidUser.test(invalidUser2));
System.out.println("Minor is valid: " + isValidUser.test(minorUser));
if (!hasValidUsername.test(invalidUser1)) {
System.out.println("Username must be at least 3 characters");
}
if (!hasValidEmail.test(invalidUser2)) {
System.out.println("Email must contain @");
}
if (!isAdult.test(minorUser)) {
System.out.println("User must be at least 18 years old");
}
}
}This approach separates validation logic from business logic, making both easier to test and maintain. Each validation rule exists as an independent, reusable predicate that you can combine in different ways for different validation contexts.
Predicates also work well for implementing filtering in search or query operations. When users need to search data with multiple optional criteria, predicates provide a clean way to build dynamic filters:
package blog.academy.javapro;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class DynamicFiltering {
static class Product {
private String name;
private String category;
private double price;
private boolean inStock;
public Product(String name, String category, double price, boolean inStock) {
this.name = name;
this.category = category;
this.price = price;
this.inStock = inStock;
}
public String getName() {
return name;
}
public String getCategory() {
return category;
}
public double getPrice() {
return price;
}
public boolean isInStock() {
return inStock;
}
@Override
public String toString() {
return String.format("%s (%s) - $%.2f %s",
name, category, price, inStock ? "[In Stock]" : "[Out of Stock]");
}
}
public static void main(String[] args) {
List<Product> products = Arrays.asList(
new Product("Laptop", "Electronics", 999.99, true),
new Product("Phone", "Electronics", 699.99, false),
new Product("Desk", "Furniture", 299.99, true),
new Product("Chair", "Furniture", 199.99, true),
new Product("Monitor", "Electronics", 349.99, true)
);
Predicate<Product> all = p -> true;
String categoryFilter = "Electronics";
Predicate<Product> categoryMatch = categoryFilter != null
? p -> p.getCategory().equals(categoryFilter)
: all;
double maxPrice = 500.0;
Predicate<Product> priceMatch = p -> p.getPrice() <= maxPrice;
Predicate<Product> stockMatch = Product::isInStock;
Predicate<Product> finalFilter = categoryMatch
.and(priceMatch)
.and(stockMatch);
List<Product> results = products.stream()
.filter(finalFilter)
.collect(Collectors.toList());
System.out.println("Filtered products:");
results.forEach(System.out::println);
}
}The Predicate functional interface provides a powerful abstraction for expressing boolean conditions in Java. By
encapsulating test logic in reusable predicates, you gain composability, clarity, and type safety. The single abstract
method test() performs the evaluation, while the default methods and(), or(), and negate() enable building
complex conditions from simple ones.
Lambda expressions make creating predicates syntactically lightweight. Rather than defining anonymous classes or
separate implementations, you express the condition directly: n -> n > 0 reads clearly and eliminates boilerplate. The
resulting predicates integrate seamlessly with the Stream API, particularly in filtering operations where predicates
select which elements to retain or process.
What makes predicates particularly valuable in production code is their support for declarative programming. Instead of writing imperative loops with conditional branches, you declare what characteristics you care about and let the language handle iteration and selection. This style produces code that's easier to read, test, and maintain. Each predicate captures a single business rule or filtering criterion, and composition methods combine them into more sophisticated conditions without increasing complexity proportionally.
The patterns you've seen here—validation, dynamic filtering, stream operations—represent just the beginning of what predicates enable. As you encounter situations where you need to evaluate conditions, ask whether a predicate would clarify the code. Often the answer is yes, and the resulting implementation will be more flexible and maintainable than traditional conditional logic. Predicates exemplify how functional programming concepts enhance object-oriented Java without replacing its core principles.