Java Predicate Interface
Test conditions on values in Java with the Predicate functional interface and its and/or/negate combinators.
Java Predicate Interface
Predicate<T> is the functional interface for the question "is this value good?" — one input of type T, one boolean answer. It sits at the heart of Stream.filter, Collection.removeIf, Optional.filter, and every JDK method that says "keep the ones that match." The interface is tiny — a single test(T) method — but it ships with a small combinator algebra (and, or, negate, isEqual, not) that lets you build complex conditions from simple ones without ever writing the boolean glue by hand.
This chapter is the same shape as the rest of Part 12's interface zoom-ins: the interface, its three or four useful methods, the algebra, then a worked example.
The interface
The whole declaration, paraphrased:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t); // the only abstract method
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 target);
static <T> Predicate<T> not(Predicate<? super T> target); // Java 11+
}test is the single abstract method that lambdas and method references implement. Everything else is built on top of it. You'll rarely call test yourself — stream().filter(...) and list.removeIf(...) call it for you — but knowing the method name matters when you write code that accepts a Predicate<T> and needs to invoke it.
Predicate<String> notBlank = s -> !s.isBlank();
boolean ok = notBlank.test("hello"); // trueand, or, negate — boolean algebra without the glue
The three default methods compose predicates the same way the &&, ||, ! operators compose booleans:
Predicate<String> notNull = Objects::nonNull;
Predicate<String> notBlank = s -> !s.isBlank();
Predicate<String> longEnough = s -> s.length() >= 3;
Predicate<String> useful = notNull.and(notBlank).and(longEnough);
Predicate<String> usableOrShort = useful.or(s -> s.length() == 1);
Predicate<String> bad = useful.negate();Two properties matter:
- Short-circuit, in declaration order.
a.and(b)only callsb.testwhena.testreturnedtrue.a.or(b)only callsb.testwhena.testreturnedfalse. That's the same evaluation order as&&and||, which means you can put cheap, common-failing checks first and the expensive ones last. - Each call returns a new
Predicate. The combinators don't mutatethis. Reuse the originals as much as you like.
negate() simply flips the result. useful.negate() returns true for nulls, blanks, and strings shorter than 3 — every case useful rejected.
Predicate.not — the readable negation
Java 11 added a static shorthand:
list.removeIf(Predicate.not(String::isBlank)); // remove every blank stringPredicate.not(p) is the same boolean answer as p.negate(), but it composes much more naturally at the call site. The method-reference form String::isBlank is a Predicate<String> on its own — but you can't write (String::isBlank).negate(), because the compiler needs a target type before it can resolve the reference. Predicate.not(String::isBlank) gives it that target type, and the whole thing reads as "not blank" in English order.
A static import of Predicate.not makes filter chains even cleaner:
import static java.util.function.Predicate.not;
...
var nonBlank = lines.stream().filter(not(String::isBlank)).toList();Predicate.isEqual — null-safe equality
Predicate<Object> isFoo = Predicate.isEqual("foo"); // o -> Objects.equals(o, "foo")The implementation is literally t -> Objects.equals(target, t), which means null on either side compares safely. It rarely saves keystrokes over s -> s.equals("foo"), but it does save you when the stream may contain null — null.equals("foo") would NPE, Objects.equals(null, "foo") returns false.
Where Predicate<T> shows up in the JDK
The same Predicate<T> flows through every "filter" API:
Stream<String> kept = stream.filter(notBlank); // Stream.filter
boolean removed = list.removeIf(String::isBlank); // Collection.removeIf
Optional<String> ok = opt.filter(notBlank); // Optional.filter
boolean any = stream.anyMatch(notBlank); // anyMatch / allMatch / noneMatch
Map<K, V> small = map.values().retainAll(p); // (via Collection.retainAll on entry set)Every one of these takes the same shape, so a Predicate<T> you build once is reusable in every direction — and assembling it from and/or/negate is exactly how you avoid the "I have three slightly different filters, all near-duplicate" smell.
Primitive specialisations — IntPredicate, LongPredicate, DoublePredicate
Predicate<Integer> works on ints, but each call boxes the input. For tight numeric pipelines the package ships:
IntPredicate even = n -> n % 2 == 0;
LongPredicate big = n -> n > 1_000_000_000L;
DoublePredicate hot = d -> d > 37.5;Same and/or/negate algebra, no boxing. These are what IntStream.filter accepts — using Predicate<Integer> there would force the stream to autobox every element on the way in.
BiPredicate<T, U> — two-argument tests
When the question takes two inputs (a key and a value, a row and a column, an old and a new), use BiPredicate:
BiPredicate<String, Integer> longEnoughFor = (s, n) -> s.length() >= n;
boolean ok = longEnoughFor.test("hello", 4); // trueThe combinator surface is smaller — and, or, negate exist, but there's no two-argument isEqual or not. Map.removeIf((k, v) -> ...) is exactly a BiPredicate<K, V>.
A worked example: predicates, composition, the algebra, and where they plug in
The program below builds three small predicates over User, composes them with and/or/negate, demonstrates short-circuiting by counting calls, swaps in Predicate.not for negation at a removeIf call site, and uses an IntPredicate against an IntStream to show the primitive variant.
What to take from the run:
- The three building-block predicates (
adult,active,namedWell) stayed reusable.eligible,minor, andreachablewere built by composition rather than by writing three separate lambdas with overlapping logic. andshort-circuited exactly the way&&does:expensiveran fewer times thancheapbecause every minor was rejected before the expensive check fired. That's the lever you have for ordering — put cheap, frequently-failing checks first.Predicate.not(...)at theremoveIfcall site read as plain English ("remove if not non-blank") and avoided needing a target type before negation. Static-importingnotis the small finishing touch.Predicate.isEqual("foo")counted the two"foo"entries past anullwithout throwing.s -> s.equals("foo")would have NPE'd on thenullelement.IntPredicate even = n -> n % 2 == 0;plugged straight intoIntStream.filterwithout boxing — and the same.and(...)combinator works on the primitive specialisation.
What's next
Predicate<T> answers yes-or-no. The next chapter, Java Function Interface, covers the interface for the other half of stream work: transforming one value into another. The shape — single-method, default-method composition (andThen, compose, plus the identity() static) — is the same as Predicate, and the same lessons about ordering, reuse, and primitive specialisations carry across.
Practice
You have `Predicate<String> notNull = Objects::nonNull;` and `Predicate<String> notBlank = s -> !s.isBlank();` and want one predicate that returns `true` only when the string is both non-null and non-blank, with `notNull` checked *first* so `notBlank` never runs on a `null`. Which expression does this correctly?