Lambdas & Functional Interfaces

Predicate & Its Combinators

15 min Lesson 4 of 13

Predicate & Its Combinators

A Predicate<T> is a functional interface in java.util.function that represents a single test: given a value of type T, return true or false. It is the standard way to express a boolean-valued condition in Java's functional API — used heavily by the Streams API, Collection methods like removeIf, and any API that needs a filter.

import java.util.function.Predicate; Predicate<String> isLong = s -> s.length() > 10; System.out.println(isLong.test("hello")); // false System.out.println(isLong.test("hello, world!")); // true

The single abstract method is boolean test(T t). Everything else on Predicateand, or, negate, not — are default or static methods that let you build compound conditions without writing nested if blocks.

Why composable predicates matter

Imagine you are filtering a list of users. Without composition you either write one giant lambda that does everything, or you write multiple for loops. With composition you define small, named predicates and wire them together — each piece is testable on its own, and the final combination reads like a sentence.

negate — flipping the result

negate() returns a new Predicate that is the logical NOT of the original.

Predicate<Integer> isEven = n -> n % 2 == 0; Predicate<Integer> isOdd = isEven.negate(); System.out.println(isOdd.test(3)); // true System.out.println(isOdd.test(4)); // false

There is also a static helper, Predicate.not(predicate), introduced in Java 11. It is especially handy inside method chains where calling .negate() would require extra parentheses:

import java.util.List; var names = List.of("Alice", "", "Bob", " ", "Charlie"); // keep only non-blank names names.stream() .filter(Predicate.not(String::isBlank)) .forEach(System.out::println); // Alice // Bob // Charlie

and — both conditions must hold

and(other) returns a new Predicate that short-circuits: if the first predicate returns false, the second is never evaluated — exactly like the && operator.

Predicate<String> notEmpty = s -> !s.isEmpty(); Predicate<String> startsWithA = s -> s.startsWith("A"); Predicate<String> validAndStartsA = notEmpty.and(startsWithA); System.out.println(validAndStartsA.test("Alice")); // true System.out.println(validAndStartsA.test("Bob")); // false System.out.println(validAndStartsA.test("")); // false (short-circuits, never checks startsWith)
Short-circuit evaluation: and stops at the first false result, and or stops at the first true result. This mirrors Java's && and || operators — order matters when a predicate has a side-effect or is expensive to evaluate.

or — at least one condition must hold

or(other) returns a Predicate that is true when either side is true.

Predicate<Integer> isNegative = n -> n < 0; Predicate<Integer> isZero = n -> n == 0; Predicate<Integer> isNonPositive = isNegative.or(isZero); System.out.println(isNonPositive.test(-5)); // true System.out.println(isNonPositive.test(0)); // true System.out.println(isNonPositive.test(3)); // false

Combining all three

The real power appears when you chain multiple combinators. Here is a realistic filtering scenario for a list of product prices:

import java.util.List; import java.util.function.Predicate; public class PriceFilter { public static void main(String[] args) { List<Double> prices = List.of(0.0, 5.99, 12.50, 99.99, 250.00, -1.0); // rules: price must be positive, at least 5.00, and below 100.00 Predicate<Double> isPositive = p -> p > 0; Predicate<Double> atLeastFive = p -> p >= 5.0; Predicate<Double> belowHundred = p -> p < 100.0; Predicate<Double> validPrice = isPositive.and(atLeastFive).and(belowHundred); prices.stream() .filter(validPrice) .forEach(p -> System.out.printf("$%.2f%n", p)); // $5.99 // $12.50 // $99.99 } }
Name your predicates. Assigning each condition to a well-named variable (like atLeastFive) makes the chain self-documenting. Avoid writing one deeply-nested lambda — it is harder to read and impossible to reuse.

Using Predicate with removeIf

Collection.removeIf(Predicate) is a convenient method that removes every element that satisfies the given predicate. It accepts any Predicate<T>, including a composed one:

import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; var numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); Predicate<Integer> isEven = n -> n % 2 == 0; Predicate<Integer> isLarge = n -> n > 7; // remove numbers that are even OR larger than 7 numbers.removeIf(isEven.or(isLarge)); System.out.println(numbers); // [1, 3, 5, 7]
Do not mutate a list while streaming it. removeIf is safe — it was designed for in-place removal. Using stream().filter(...) does not modify the original list; it produces a new stream. Know which behaviour you need before choosing between the two.

Summary

Predicate<T> turns a boolean condition into a first-class object. Its four key operations are:

  • test(t) — evaluate the condition.
  • negate() / Predicate.not(...) — logical NOT.
  • and(other) — logical AND with short-circuit.
  • or(other) — logical OR with short-circuit.

Building complex filters from small, named predicates is idiomatic Java 8+ style — it is composable, readable, and testable. You will see this pattern constantly when we reach the Streams lesson.