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.

MethodJob
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.

APISlotTypical lambda
Stream.filterPredicate<? super T>Order::active
Stream.anyMatch / allMatch / noneMatchPredicate<? super T>o -> o.total().signum() > 0
Optional.filterPredicate<? super T>Order::active
Collection.removeIfPredicate<? super E>id -> id.startsWith("tmp-")
Collectors.partitioningByPredicate<? 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 / anyMatch with Predicate (or a method reference that matches it).
  • Compose small named predicates with and / or instead of growing an if.
  • Use Predicate.not when you need the inverse in a slot that wants a Predicate.

Don’t:

  • Log, persist, or mutate inside test.
  • Put the expensive half of an and first.
  • Invent ActiveOrderPredicateImpl for a slot that already takes Predicate.

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.

Next optional step in the series Accept a value in order to log, persist, or notify — not to map it. Consumer: Accept a Value for a Side Effect