Java Tutorial
🔍

Java Predicate Interface

Java Predicate Interface

Predicate<T> is the functional interface for a single-argument condition that returns true or false. It exists because so much everyday code reduces to the same shape — does this value satisfy some rule — and writing a brand new interface every time that pattern shows up would be needless repetition. Java 8 shipped Predicate<T> in java.util.function with exactly one abstract method, test(T t), alongside a handful of default methods that let several predicates combine into a bigger rule without any of them changing.

What Is Predicate?

Predicate<T> declares boolean test(T value) as its single abstract method, which makes it a valid target for any lambda or method reference that takes one argument of type T and returns a boolean. It also carries default methods — and, or, negate — and a couple of static ones — not, isEqual — that exist specifically to combine predicates without writing a bigger, more tangled condition by hand.

It shows up constantly wherever Java code needs to ask "does this qualify" — Stream.filter(), Collection.removeIf(), and any method you write that accepts a reusable condition as a parameter instead of a single hardcoded check.

Why Predicate Was Introduced

Before Java 8, a filtering rule lived inside a loop, checked with a plain if statement, with no way to hand that rule to another method or reuse it somewhere else without copying the condition itself.

1// File: BeforePredicate.java 2import java.util.*; 3 4public class BeforePredicate { 5 public static void main(String[] args) { 6 List<Integer> orderAmounts = new ArrayList<>(List.of(80, 320, 150, 45, 600)); 7 8 List<Integer> qualifyingOrders = new ArrayList<>(); 9 for (Integer amount : orderAmounts) { 10 if (amount >= 150) { 11 qualifyingOrders.add(amount); 12 } 13 } 14 15 System.out.println(qualifyingOrders); 16 } 17}
Output:
[320, 150, 600]

The same rule expressed as a Predicate<Integer> becomes a value on its own — something that can be passed to filter, stored in a variable, reused across several methods, and combined with other rules later without ever touching the loop that consumes it.

1// File: AfterPredicate.java 2import java.util.*; 3import java.util.function.*; 4import java.util.stream.*; 5 6public class AfterPredicate { 7 public static void main(String[] args) { 8 List<Integer> orderAmounts = new ArrayList<>(List.of(80, 320, 150, 45, 600)); 9 10 Predicate<Integer> meetsMinimum = amount -> amount >= 150; 11 12 List<Integer> qualifyingOrders = orderAmounts.stream() 13 .filter(meetsMinimum) 14 .collect(Collectors.toList()); 15 16 System.out.println(qualifyingOrders); 17 } 18}
Output:
[320, 150, 600]

The output is identical either way. What changed is that meetsMinimum is now a named, reusable value instead of logic buried inside one loop.

Syntax

Predicate<T> combines one abstract method with default methods for boolean composition and static methods for two common patterns.

1// File: PredicateSyntaxForms.java 2import java.util.function.*; 3 4public class PredicateSyntaxForms { 5 public static void main(String[] args) { 6 Predicate<Integer> isPositive = value -> value > 0; 7 Predicate<Integer> isEven = value -> value % 2 == 0; 8 9 Predicate<Integer> positiveAndEven = isPositive.and(isEven); 10 Predicate<Integer> positiveOrEven = isPositive.or(isEven); 11 Predicate<Integer> notPositive = isPositive.negate(); 12 Predicate<Integer> notEvenStatic = Predicate.not(isEven); 13 Predicate<Object> equalsFive = Predicate.isEqual(5); 14 15 System.out.println("4 positive and even: " + positiveAndEven.test(4)); 16 System.out.println("3 positive and even: " + positiveAndEven.test(3)); 17 System.out.println("-4 positive or even: " + positiveOrEven.test(-4)); 18 System.out.println("-3 positive or even: " + positiveOrEven.test(-3)); 19 System.out.println("-3 not positive: " + notPositive.test(-3)); 20 System.out.println("7 not even (static): " + notEvenStatic.test(7)); 21 System.out.println("isEqual 5 against 6: " + equalsFive.test(6)); 22 } 23}
Output:
4 positive and even: true
3 positive and even: false
-4 positive or even: true
-3 positive or even: false
-3 not positive: true
7 not even (static): true
isEqual 5 against 6: false

Common Use Cases

Filtering a Stream

Stream.filter is the single most common place a Predicate appears in real code, keeping the filtering rule readable as a named value instead of an inline expression buried inside the pipeline.

1// File: StreamFilterPredicateExample.java 2import java.util.*; 3import java.util.function.*; 4import java.util.stream.*; 5 6public class StreamFilterPredicateExample { 7 public static void main(String[] args) { 8 List<String> productNames = List.of("Laptop", "Mouse", "Keyboard", "Monitor", "Mic"); 9 10 Predicate<String> longName = name -> name.length() > 5; 11 12 List<String> filtered = productNames.stream() 13 .filter(longName) 14 .collect(Collectors.toList()); 15 16 System.out.println(filtered); 17 } 18}
Output:
[Laptop, Keyboard, Monitor]

Removing Elements That Match a Condition

Collection.removeIf takes a Predicate directly and removes every matching element in place, which reads more clearly than a manual iterator loop written to avoid a ConcurrentModificationException.

1// File: RemoveIfPredicateExample.java 2import java.util.*; 3import java.util.function.*; 4 5public class RemoveIfPredicateExample { 6 public static void main(String[] args) { 7 List<Integer> stockLevels = new ArrayList<>(List.of(120, 0, 45, 0, 300)); 8 9 Predicate<Integer> outOfStock = quantity -> quantity == 0; 10 11 stockLevels.removeIf(outOfStock); 12 13 System.out.println(stockLevels); 14 } 15}
Output:
[120, 45, 300]

Combining Smaller Rules Into a Bigger One

and() builds a stricter condition out of two smaller ones, and it short-circuits exactly the way the && operator does — the second predicate never runs once the first has already returned false.

1// File: CombinedPredicateExample.java 2import java.util.function.*; 3 4public class CombinedPredicateExample { 5 public static void main(String[] args) { 6 Predicate<String> hasMinLength = value -> value.length() >= 8; 7 Predicate<String> hasDigit = value -> value.chars().anyMatch(Character::isDigit); 8 9 Predicate<String> isStrongPassword = hasMinLength.and(hasDigit); 10 11 System.out.println("Password123 is strong: " + isStrongPassword.test("Password123")); 12 System.out.println("weak is strong: " + isStrongPassword.test("weak")); 13 } 14}
Output:
Password123 is strong: true
weak is strong: false

Accepting a Predicate as a Reusable Rule

Passing a Predicate into a method turns that method into something generic and reusable, instead of hardcoding one specific condition it can never be used for again.

1// File: PredicateAsParameterExample.java 2import java.util.*; 3import java.util.function.*; 4 5public class PredicateAsParameterExample { 6 static <T> long countMatching(List<T> items, Predicate<T> condition) { 7 long count = 0; 8 for (T item : items) { 9 if (condition.test(item)) { 10 count++; 11 } 12 } 13 return count; 14 } 15 16 public static void main(String[] args) { 17 List<Integer> ages = List.of(17, 25, 16, 42, 19, 15); 18 19 long adultCount = countMatching(ages, age -> age >= 18); 20 System.out.println("Adults: " + adultCount); 21 } 22}
Output:
Adults: 3

Real-World Example

A food delivery platform typically has to check several unrelated conditions before an order is confirmed as deliverable — is the customer within the service radius, is the restaurant currently open, does the order meet the minimum amount, and is the customer blacklisted. Writing all four checks inline inside one method makes that method grow every single time a new business rule shows up. Declaring each condition as a named Predicate and combining them with and, or, and not keeps every rule readable, testable, and reusable on its own.

1// File: DeliveryOrder.java 2 3public class DeliveryOrder { 4 private final String orderId; 5 private final double distanceKm; 6 private final double orderAmount; 7 private final boolean restaurantOpen; 8 private final boolean blacklistedCustomer; 9 private final boolean premiumMember; 10 11 public DeliveryOrder(String orderId, double distanceKm, double orderAmount, 12 boolean restaurantOpen, boolean blacklistedCustomer, boolean premiumMember) { 13 this.orderId = orderId; 14 this.distanceKm = distanceKm; 15 this.orderAmount = orderAmount; 16 this.restaurantOpen = restaurantOpen; 17 this.blacklistedCustomer = blacklistedCustomer; 18 this.premiumMember = premiumMember; 19 } 20 21 public double getDistanceKm() { 22 return distanceKm; 23 } 24 25 public double getOrderAmount() { 26 return orderAmount; 27 } 28 29 public boolean isRestaurantOpen() { 30 return restaurantOpen; 31 } 32 33 public boolean isBlacklistedCustomer() { 34 return blacklistedCustomer; 35 } 36 37 public boolean isPremiumMember() { 38 return premiumMember; 39 } 40}
1// File: EligibilityRules.java 2import java.util.function.Predicate; 3 4public class EligibilityRules { 5 static final double MAX_DELIVERY_DISTANCE_KM = 8.0; 6 static final double MINIMUM_ORDER_AMOUNT = 150.0; 7 static final double FREE_DELIVERY_THRESHOLD = 500.0; 8 9 static final Predicate<DeliveryOrder> withinServiceArea = 10 order -> order.getDistanceKm() <= MAX_DELIVERY_DISTANCE_KM; 11 12 static final Predicate<DeliveryOrder> restaurantIsOpen = DeliveryOrder::isRestaurantOpen; 13 14 static final Predicate<DeliveryOrder> meetsMinimumAmount = 15 order -> order.getOrderAmount() >= MINIMUM_ORDER_AMOUNT; 16 17 static final Predicate<DeliveryOrder> notBlacklisted = 18 Predicate.not(DeliveryOrder::isBlacklistedCustomer); 19 20 static final Predicate<DeliveryOrder> highValueOrder = 21 order -> order.getOrderAmount() >= FREE_DELIVERY_THRESHOLD; 22 23 static final Predicate<DeliveryOrder> qualifiesForFreeDelivery = 24 highValueOrder.or(DeliveryOrder::isPremiumMember); 25 26 static final Predicate<DeliveryOrder> isEligibleForDelivery = 27 withinServiceArea.and(restaurantIsOpen).and(meetsMinimumAmount).and(notBlacklisted); 28}
1// File: EligibilityChecker.java 2 3public class EligibilityChecker { 4 public boolean canDeliver(DeliveryOrder order) { 5 return EligibilityRules.isEligibleForDelivery.test(order); 6 } 7 8 public boolean isFreeDelivery(DeliveryOrder order) { 9 return EligibilityRules.qualifiesForFreeDelivery.test(order); 10 } 11}
1// File: EligibilityDemo.java 2 3public class EligibilityDemo { 4 public static void main(String[] args) { 5 EligibilityChecker checker = new EligibilityChecker(); 6 7 DeliveryOrder nearbyOrder = new DeliveryOrder("ORD-1", 4.5, 320.0, true, false, false); 8 DeliveryOrder tooFarOrder = new DeliveryOrder("ORD-2", 12.0, 600.0, true, false, false); 9 DeliveryOrder lowValueOrder = new DeliveryOrder("ORD-3", 3.0, 90.0, true, false, false); 10 DeliveryOrder premiumSmallOrder = new DeliveryOrder("ORD-4", 5.0, 200.0, true, false, true); 11 12 System.out.println("ORD-1 eligible: " + checker.canDeliver(nearbyOrder)); 13 System.out.println("ORD-2 eligible: " + checker.canDeliver(tooFarOrder)); 14 System.out.println("ORD-3 eligible: " + checker.canDeliver(lowValueOrder)); 15 16 System.out.println("ORD-1 free delivery: " + checker.isFreeDelivery(nearbyOrder)); 17 System.out.println("ORD-4 free delivery: " + checker.isFreeDelivery(premiumSmallOrder)); 18 } 19}
Output:
ORD-1 eligible: true
ORD-2 eligible: false
ORD-3 eligible: false
ORD-1 free delivery: false
ORD-4 free delivery: true

Reading isEligibleForDelivery top to bottom in EligibilityRules tells you exactly what qualifies an order without opening a single method body — each rule already carries a name that says what it checks. A mistake that appears often in fresher pull requests is folding all four of these checks back into one long if condition inside EligibilityChecker the moment a fifth rule needs adding, which quietly throws away the entire readability benefit this design was built for.

Combining Predicate With Other Features

Predicate composes naturally with Stream.filter and Collection.removeIf, and its and, or, and negate methods let a handful of small, well-named predicates build up into a rule as complex as an entire eligibility check without ever declaring a new interface. Method references pair especially well with Predicate for simple property checks, exactly as DeliveryOrder::isRestaurantOpen does above. For a condition that needs two arguments instead of one, BiPredicate<T, U> fills the same role with an extra parameter.

Best Practices

Give every meaningful predicate a name, the way withinServiceArea and restaurantIsOpen are named above, rather than inlining the condition directly into filter or an if statement. A named predicate reads like a line of documentation the next time someone opens the file.

Reach for Predicate.not() when negating a bare method reference, since a method reference has no negate() method to call until the compiler has already resolved it into a Predicate. negate() still works fine on an existing Predicate variable — the static form exists specifically for the method reference case.

Keep test() free of side effects. A predicate should only answer a question about the value it receives, never mutate that value, a shared field, or anything else — a predicate with side effects behaves unpredictably the moment it runs inside a parallel stream or gets evaluated more than once.

Common Mistakes

Calling .negate() directly on a bare method reference does not compile, because a method reference has no methods of its own until it has already been resolved into a Predicate.

1// File: NegateOnBareReferenceMistake.java 2import java.util.function.*; 3 4public class NegateOnBareReferenceMistake { 5 static boolean isBlacklisted(String customerId) { 6 return customerId.equals("BLOCKED-1"); 7 } 8 9 public static void main(String[] args) { 10 // Predicate<String> notBlacklisted = NegateOnBareReferenceMistake::isBlacklisted.negate(); 11 // This does not compile - a bare method reference has no negate() 12 // method until the compiler has already resolved it into a Predicate 13 14 Predicate<String> notBlacklisted = Predicate.not(NegateOnBareReferenceMistake::isBlacklisted); 15 System.out.println("BLOCKED-1 allowed: " + notBlacklisted.test("BLOCKED-1")); 16 System.out.println("CUST-99 allowed: " + notBlacklisted.test("CUST-99")); 17 } 18}
Output:
BLOCKED-1 allowed: false
CUST-99 allowed: true

Chaining and() and or() without thinking about grouping produces a different result than most people expect, because each call combines strictly left to right rather than following any implicit "and binds tighter than or" rule the way Java's && and || operators do.

1// File: PredicateChainingPrecedenceMistake.java 2import java.util.function.*; 3 4public class PredicateChainingPrecedenceMistake { 5 public static void main(String[] args) { 6 Predicate<Integer> isEven = value -> value % 2 == 0; 7 Predicate<Integer> isNegative = value -> value < 0; 8 Predicate<Integer> isLarge = value -> value > 100; 9 10 // Evaluates as (isEven OR isNegative) AND isLarge - grouped left to right 11 Predicate<Integer> chainedLeftToRight = isEven.or(isNegative).and(isLarge); 12 13 // Evaluates as isEven OR (isNegative AND isLarge) - grouped differently 14 Predicate<Integer> chainedDifferently = isEven.or(isNegative.and(isLarge)); 15 16 System.out.println("chainedLeftToRight.test(50): " + chainedLeftToRight.test(50)); 17 System.out.println("chainedDifferently.test(50): " + chainedDifferently.test(50)); 18 } 19}
Output:
chainedLeftToRight.test(50): false
chainedDifferently.test(50): true

A predicate that quietly captures mutable state, such as a counter incremented inside test(), stops being safely reusable the moment it runs inside a parallel stream or gets called from more than one place at the same time. This mistake rarely shows up in a single-threaded test locally, which is exactly why it tends to survive code review and surface later as an intermittent production bug instead.

Interview Questions

Q1. What is Predicate in Java, and where does it fit in java.util.function?

Predicate<T> is the functional interface for a single-argument condition that returns a boolean, declaring test(T value) as its one abstract method. It belongs to the java.util.function package alongside Function, Consumer, and Supplier, and it is the interface behind every lambda passed to Stream.filter or Collection.removeIf. Interviewers commonly use this as a warm-up question to confirm the candidate can connect the interface name to its actual method signature rather than describing it only in the abstract.

Q2. What is the difference between Predicate.negate() and Predicate.not()?

negate() is an instance method called on an already-existing Predicate, returning a new predicate that inverts its result. Predicate.not() is a static method, added in Java 11, that does the same inversion but accepts a method reference or lambda directly, before it has been assigned to a Predicate variable — this matters because a bare method reference has no negate() method of its own to call.

Q3. Do and() and or() short-circuit the way the && and || operators do?

Yes. and() stops evaluating and returns false as soon as the first predicate returns false, and or() stops and returns true as soon as the first predicate returns true — the second predicate in each case never runs once the outcome is already decided. This matters in practice whenever the second predicate is more expensive to evaluate or depends on state the first predicate is responsible for validating first.

Q4. How would you combine three or more predicates while controlling evaluation order explicitly?

Chain and() and or() calls in the exact order the logic requires, using parentheses around sub-expressions the same way you would with boolean operators, since Predicate composition has no implicit operator precedence of its own — each call simply combines with whatever came immediately before it. Product-based interviews often follow up by asking the candidate to trace through a chained expression like a.or(b).and(c) versus a.or(b.and(c)) to confirm they understand the grouping is entirely explicit, not inferred.

Q5. What is the difference between Predicate and BiPredicate?

Predicate<T> tests a single argument through test(T value), while BiPredicate<T, U> tests two arguments of potentially different types through test(T first, U second) — comparing two values against each other, for example, rather than checking one value against a fixed rule. Both interfaces expose the same and, or, and negate default methods for combining rules.

Q6. Can a Predicate maintain state across multiple test() calls, and is that good practice?

Technically yes, since nothing prevents a lambda from capturing and mutating an outside variable or an instance field inside test(), but it is considered poor practice because it breaks the assumption that a predicate is a pure, repeatable check. A stateful predicate produces different results for the same input depending on when it runs, which becomes especially dangerous inside a parallel stream where multiple threads may call test() concurrently on the same predicate instance.

FAQs

Can Predicate be used with primitives directly?

Not without autoboxing. Predicate<Integer> boxes every int into an Integer before testing it, which is fine for most everyday code, but java.util.function also ships IntPredicate, LongPredicate, and DoublePredicate specifically to avoid that boxing cost when it matters.

What is Predicate.isEqual used for?

Predicate.isEqual(target) builds a predicate that checks whether its input equals a fixed target object, using Objects.equals internally so it also handles null safely. It is most often used inside filter() when the condition is a simple equality check against one known value, rather than a custom comparison rule.

Is Predicate the same as a boolean-returning Function?

Conceptually similar but not interchangeable. Function<T, Boolean> returns a boxed Boolean and has no and, or, or negate methods, while Predicate<T> returns a primitive boolean and ships with all three composition methods built in specifically for combining conditions.

Can I chain more than two predicates together?

Yes, without any limit. Each call to and() or or() returns a new Predicate, so chaining a.and(b).and(c).and(d) combines any number of conditions, one call at a time, in the exact order they are written.

Does filter() with a Predicate create a new list immediately?

No. Stream.filter is a lazy, intermediate operation — nothing runs until a terminal operation like collect or forEach is called on the stream, at which point the predicate is evaluated once per element as the stream actually processes them.

Can a Predicate be null-safe by default?

No, and this trips up plenty of beginners. Calling test(null) runs whatever logic the predicate's body contains against null, and it throws a NullPointerException the moment that body tries to call a method on the null value — the predicate itself does not add any automatic null handling.

What is IntPredicate and why does it exist?

IntPredicate is the primitive-specialized version of Predicate<Integer>, declaring test(int value) instead of test(Integer value). It exists purely to avoid the autoboxing cost of wrapping every int into an Integer object, which matters most in performance-sensitive code processing large volumes of primitive values.

Summary

Predicate<T> gives every "does this qualify" check in your codebase a shared, reusable shape instead of a fresh if statement written from scratch each time. test() asks the question, and and, or, negate, and not let several small, well-named predicates build into a bigger rule without any of the smaller ones changing — exactly the pattern the delivery eligibility example leans on.

The two things worth carrying forward are that predicate chaining is always evaluated strictly in the order it is written, with no implicit precedence to rely on, and that a predicate should stay a pure check with no side effects, since that purity is what makes it safe to reuse anywhere in the codebase without surprises. Function, Consumer, and Supplier follow the exact same design instincts from here, just answering different questions than "true or false."

What to Read Next