You need the active orders, or the ones over a threshold, or the ones that are both. The classic path is a growing if inside a loop, then a second loop for a second question, then a boolean flag that means “I already decided.” What you wanted was a yes/no you could name, pass, and combine.
Predicate<T> is that yes/no.
A Predicate answers a yes/no question about a value. The series hub owns the glossary; here we only care about test, the composition defaults, isEqual, Predicate.not, and the slots that already take a Predicate.
The filter you used to inline
Before a Predicate slot existed, “active and billable” usually lived in the loop:
List<Order> keep = new ArrayList<>();
for (Order order : orders) {
if (order.active() && order.total().compareTo(new BigDecimal("10")) >= 0) {
keep.add(order);
}
}
Fine for one condition that never moves. Noisy when the same question is reused in a Stream, a removeIf, and a unit test:
Predicate<Order> active = Order::active;
Predicate<Order> billable = order -> order.total().compareTo(new BigDecimal("10")) >= 0;
Predicate<Order> activeAndBillable = active.and(billable);
List<Order> keep = orders.stream()
.filter(activeAndBillable)
.toList();
The combination is a Predicate too. You can pass activeAndBillable into anything that takes one. Streams filter is the slot you already know; this post is the type that fills it.
The contract
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) { … }
default Predicate<T> or(Predicate<? super T> other) { … }
default Predicate<T> negate() { … }
static <T> Predicate<T> isEqual(Object targetRef) { … }
static <T> Predicate<T> not(Predicate<? super T> target) { … } // Java 11
}
test is the SAM. The rest are defaults and static factories; they do not stop you from writing a lambda.
| Method | Job |
|---|---|
test(t) | Answer yes or no |
and(other) | This and other (short-circuits on false) |
or(other) | This or other (short-circuits on true) |
negate() | Flip this predicate |
isEqual(ref) | Objects.equals(ref, t) as a Predicate |
Predicate.not(p) | Same as p.negate(), readable in a filter slot (Java 11) |
and and or short-circuit the same way && and || do. If active.test(order) is false, billable is never called. That matters when the second test is expensive (a repository hit you should not have put in a Predicate in the first place — but if you did, short-circuit still saves you).
Predicate<Order> notActive = Predicate.not(Order::active);
// Java 8 equivalent: ((Predicate<Order>) Order::active).negate()
Predicate.not exists because Order::active.negate() does not compile — a method reference has no type yet, so it has no negate. Cast or wrap; not is the wrap the JDK added in 11.
isEqual is the “is this the same value” predicate without a lambda:
Predicate<String> isGuest = Predicate.isEqual("guest@shop.local");
Null-safe: Predicate.isEqual(null) tests for null with == semantics via Objects.equals.
Where the JDK already asks for Predicate
You do not invent a filter type for these slots. You fill them.
| API | Slot | Typical lambda |
|---|---|---|
Stream.filter | Predicate<? super T> | Order::active |
Stream.anyMatch / allMatch / noneMatch | Predicate<? super T> | o -> o.total().signum() > 0 |
Optional.filter | Predicate<? super T> | Order::active |
Collection.removeIf | Predicate<? super E> | id -> id.startsWith("tmp-") |
Collectors.partitioningBy | Predicate<? super T> | Order::active |
removeIf is the in-place cousin of filter. It mutates the collection; filter builds a new stream. Pick the one that matches whether you own the list:
orders.removeIf(Predicate.not(Order::active));
partitioningBy splits into a Map<Boolean, List<T>> — the classifier is still a Predicate:
Map<Boolean, List<Order>> byActive = orders.stream()
.collect(Collectors.partitioningBy(Order::active));
Combining predicates without a boolean soup
Named predicates compose better than a five-clause if. Keep each question small, then and / or at the call site:
Predicate<Order> active = Order::active;
Predicate<Order> hasEmail = order -> order.customerEmail() != null
&& !order.customerEmail().isBlank();
Predicate<Order> overThreshold = order ->
order.total().compareTo(new BigDecimal("10")) >= 0;
Predicate<Order> readyToCharge = active.and(hasEmail).and(overThreshold);
Predicate<Order> skip = readyToCharge.negate();
That is still one Predicate. You can pass readyToCharge into filter, into a test, or into a method that should not know about emails and thresholds.
When the question is a domain verb other people will implement, name it the same way Strategy names DiscountPolicy. Use Predicate<Order> when the JDK already declared that slot.
Pitfalls
Side effects in test. A predicate that writes a log line or increments a counter is a Consumer in disguise. filter may skip elements (anyMatch short-circuits; a parallel stream may test in another order). Do not put business side effects inside test.
Checked exceptions. test does not declare them. Same boundary rule as Function: wrap or preprocess.
Predicate.not vs negate. not is static and Java 11+. negate is an instance method. Target typing is on the hub; here the rule is that Order::active.negate() has no type yet, so it has no negate. Assign to a Predicate<Order> first, or use Predicate.not(Order::active) in a filter slot.
Expensive tests last. and / or short-circuit, but filter(a.and(b)) still runs a then b per element. Put the cheap check first, the same way you would with &&. Streams Advanced makes the same point for pipeline order.
Cheat sheet
SAM boolean test(T t)
Defaults and or negate (short-circuit like && / ||)
Statics isEqual(ref) Predicate.not(p) // not: Java 11
JDK slots Stream.filter / anyMatch / allMatch / noneMatch
Optional.filter Collection.removeIf
Collectors.partitioningBy
Do Order::active in a filter slot compose with and / or
Predicate.not(Order::active) Java 11
Don't side effects in test empty-catch inside test
Order::active.negate() no target type yet
Do:
- Fill
filter/removeIf/anyMatchwithPredicate(or a method reference that matches it). - Compose small named predicates with
and/orinstead of growing anif. - Use
Predicate.notwhen you need the inverse in a slot that wants aPredicate.
Don’t:
- Log, persist, or mutate inside
test. - Put the expensive half of an
andfirst. - Invent
ActiveOrderPredicateImplfor a slot that already takesPredicate.
Wrap-up
Predicate<T> is the JDK name for a yes/no about one value. test is the SAM; and / or / negate compose questions; Predicate.not is the readable inverse since Java 11. You meet it first in Stream.filter. The next shape does not return a value — it does something with one.