☕ Java

Predicate

Predicate<T> is a functional interface in java.util.function representing a boolean-valued function of one argument, with the single abstract method boolean test(T t). It is one of the four foundational functional interfaces in the Java standard library and is used throughout the Collections framework, Streams API, and Optional for filtering, condition testing, and validation. Predicate is designed for composition: its default methods and(Predicate), or(Predicate), and negate() allow building complex boolean expressions from simple predicates without boilerplate. The static methods isEqual(Object) and not(Predicate) provide factory methods for common cases. The primitive specializations IntPredicate, LongPredicate, and DoublePredicate avoid boxing overhead for numeric values. BiPredicate<T,U> extends the concept to two-argument boolean functions. This entry covers the complete Predicate API, all composition methods and their short-circuit semantics, the static factory methods, primitive specializations, BiPredicate, using Predicate in stream pipelines and Collections methods, building validation frameworks with Predicate composition, and the performance and readability trade-offs of different composition styles.

Predicate API — test(), and(), or(), negate(), and Static Methods

Predicate<T> has one abstract method: boolean test(T t). This method receives a value of type T and returns a boolean. When used as a lambda, the lambda body must evaluate to boolean. When used as a method reference, the referenced method must have the same signature — taking a T and returning boolean. Common examples: String::isEmpty is a Predicate<String> because isEmpty() takes no arguments on its receiver (the String instance), matching the functional signature T → boolean where T = String. Objects::nonNull is a Predicate<Object> because nonNull(Object) returns boolean. and(Predicate<? super T> other) returns a new Predicate that evaluates this.test(t) AND other.test(t) with short-circuit evaluation: if this returns false, other is not called. The wildcard ? super T allows combining predicates of supertypes: a Predicate<Number> can be combined with a Predicate<Integer> using and() because Number is a supertype of Integer. The result is a Predicate<Integer>. or(Predicate<? super T> other) returns a new Predicate with short-circuit OR: if this returns true, other is not called. Same wildcard semantics as and(). negate() returns a new Predicate that is the logical complement of this predicate. !predicate.test(t) and predicate.negate().test(t) are equivalent, but the negate() form can be stored, passed, and composed. Predicate.not(Predicate<? super T> target) (Java 11+) is a static method that returns a predicate that is the negation of target. It is designed specifically to make method reference negation readable: Predicate.not(String::isBlank) is more readable than s -> !s.isBlank() and more composable than writing an inline negation. Predicate.isEqual(Object targetRef) returns a predicate that tests whether the argument is equal (via Objects.equals()) to targetRef — useful for filtering by a known value. The composition methods do not evaluate immediately — they create new Predicate objects that capture the original predicates as lambdas. The resulting chain evaluates lazily when test() is called. The depth of composition is limited only by stack space; in practice, chains of 5-15 predicates are common in validation frameworks.
Java
// ── Basic Predicate creation ──────────────────────────────────────────
Predicate<String> notBlank     = s -> !s.isBlank();
Predicate<String> shortEnough  = s -> s.length() <= 100;
Predicate<String> startsUpper  = s -> Character.isUpperCase(s.charAt(0));
Predicate<Integer> positive    = n -> n > 0;
Predicate<Integer> even        = n -> n % 2 == 0;

System.out.println(notBlank.test("hello"));    // true
System.out.println(notBlank.test("   "));      // false
System.out.println(positive.test(-5));         // false
System.out.println(even.test(4));              // true

// ── Method references as Predicates ───────────────────────────────────
Predicate<String>  isEmpty     = String::isEmpty;       // T → boolean
Predicate<Object>  isNull      = Objects::isNull;
Predicate<Object>  nonNull     = Objects::nonNull;
Predicate<String>  matches     = s -> s.matches("[A-Z][a-z]+");

// ── and() — short-circuit AND ─────────────────────────────────────────
Predicate<String> validInput = notBlank.and(shortEnough).and(startsUpper);

System.out.println(validInput.test("Hello"));         // true
System.out.println(validInput.test("hello"));         // false (doesn't start uppercase)
System.out.println(validInput.test(""));              // false (blank, short-circuits here)
System.out.println(validInput.test("H".repeat(200))); // false (too long)

// Short-circuit: if notBlank returns false, shortEnough is NOT called:
Predicate<String> sideEffectAnd = notBlank.and(s -> {
    System.out.println("  Checking length of: " + s);
    return s.length() <= 5;
});
sideEffectAnd.test("");     // false — no "Checking length" printed (short-circuited)
sideEffectAnd.test("hi");   // true"Checking length" printed (and() evaluated)

// ── or() — short-circuit OR ───────────────────────────────────────────
Predicate<Integer> positiveOrEven = positive.or(even);
System.out.println(positiveOrEven.test(3));   // true  (positive, even not checked)
System.out.println(positiveOrEven.test(-4));  // true  (not positive, but even)
System.out.println(positiveOrEven.test(-3));  // false (neither)

// ── negate() ─────────────────────────────────────────────────────────
Predicate<Integer> notPositive = positive.negate();
Predicate<String>  notEmpty    = isEmpty.negate();

System.out.println(notPositive.test(5));   // false
System.out.println(notPositive.test(-3));  // true
System.out.println(notEmpty.test("hi"));   // true
System.out.println(notEmpty.test(""));     // false

// ── Predicate.not() — Java 11+, for readable negation of method refs ──
// Before Java 11 — ugly:
Predicate<String> notBlank1 = s -> !s.isBlank();

// Java 11+ — cleaner:
Predicate<String> notBlank2 = Predicate.not(String::isBlank);

// In stream pipelines this is most readable:
List<String> lines = List.of("hello", "  ", "world", "", "java");
List<String> nonBlanks = lines.stream()
    .filter(Predicate.not(String::isBlank))  // cleaner than s -> !s.isBlank()
    .collect(Collectors.toList());
System.out.println(nonBlanks);  // [hello, world, java]

// ── Predicate.isEqual() — equality testing ────────────────────────────
Predicate<String> isHello = Predicate.isEqual("hello");
System.out.println(isHello.test("hello"));  // true
System.out.println(isHello.test("world"));  // false
System.out.println(isHello.test(null));     // false (Objects.equals handles null)

// Useful for filtering a specific value:
List<String> words = List.of("hello", "world", "hello", "java");
long helloCount = words.stream().filter(Predicate.isEqual("hello")).count();
System.out.println(helloCount);  // 2

Predicate in Streams, Collections, and Validation Frameworks

Predicate is the central parameter type in stream filtering and Collection searching. Stream.filter(Predicate<? super T>) is the primary use: it keeps only elements for which the predicate returns true. removeIf(Predicate<? super E>) on List, Set, and Map.Entry removes elements matching the predicate. Collection.removeIf() modifies the collection in place and returns true if any elements were removed. Optional.filter(Predicate<? super T>) returns an Optional containing the value if the predicate passes, or empty if it fails — this is the standard way to conditionally transform Optional values. Predicate composition shines in validation scenarios where multiple conditions must be checked. A validation framework built on Predicate avoids if-else chains and produces composable, reusable validation rules. The pattern: define individual rules as Predicate instances, compose them with and() for "all must pass" validation or or() for "at least one must pass", wrap them in a Validator class that collects the failed rules and produces error messages. The key readability benefit of Predicate composition over if-else chains is that each condition is a named, testable object. isNotBlank.and(isShortEnough).and(hasValidCharacters) reads as documentation of the validation logic. Each predicate can be tested in isolation in unit tests. New conditions can be added without modifying existing code — open/closed principle applied to validation. The short-circuit property of and() and or() is both a performance feature and a correctness concern. In validation: if the first predicate in an and() chain is a null check, and a subsequent predicate would throw NullPointerException on null input, the short-circuit prevents the exception. This is the standard pattern for safe predicate chaining: Objects::nonNull.and(moreSpecificPredicate) ensures moreSpecificPredicate is only called on non-null values. The analogous pattern for or() is less common but arises in fallback validation: strictRule.or(lenientFallback) applies lenientFallback only when strictRule fails.
Java
// ── Predicate in Stream.filter() ──────────────────────────────────────
List<Integer> numbers = List.of(1, -2, 3, -4, 5, -6, 7, 8, -9, 10);

// Single predicate:
List<Integer> positives = numbers.stream()
    .filter(n -> n > 0)
    .collect(Collectors.toList());
System.out.println(positives);  // [1, 3, 5, 7, 8, 10]

// Composed predicate:
Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isEven     = n -> n % 2 == 0;

List<Integer> positiveEvens = numbers.stream()
    .filter(isPositive.and(isEven))
    .collect(Collectors.toList());
System.out.println(positiveEvens);  // [8, 10]

// Negate to get non-positives:
List<Integer> nonPositives = numbers.stream()
    .filter(isPositive.negate())
    .collect(Collectors.toList());
System.out.println(nonPositives);  // [-2, -4, -6, -9]

// ── Collection.removeIf() ─────────────────────────────────────────────
List<String> mutableList = new ArrayList<>(
    List.of("apple", "", "banana", "  ", "cherry", "date")
);

mutableList.removeIf(String::isBlank);   // removes "" and "  "
System.out.println(mutableList);  // [apple, banana, cherry, date]

// removeIf with composed predicate:
mutableList.removeIf(s -> s.length() <= 4);  // removes "date"
System.out.println(mutableList);  // [apple, banana, cherry]

// ── Optional.filter() ─────────────────────────────────────────────────
Optional<String> opt1 = Optional.of("hello").filter(s -> s.length() > 3);
Optional<String> opt2 = Optional.of("hi").filter(s -> s.length() > 3);

System.out.println(opt1);  // Optional[hello]
System.out.println(opt2);  // Optional.empty

// Chained Optional filter + map:
Optional<Integer> result = Optional.of("  hello  ")
    .map(String::trim)
    .filter(Predicate.not(String::isEmpty))
    .map(String::length);
System.out.println(result);  // Optional[5]

// ── Validation framework with Predicate composition ────────────────────
record ValidationRule<T>(Predicate<T> condition, String errorMessage) {
    boolean check(T value) { return condition.test(value); }
}

class Validator<T> {
    private final List<ValidationRule<T>> rules = new ArrayList<>();

    Validator<T> require(Predicate<T> condition, String message) {
        rules.add(new ValidationRule<>(condition, message));
        return this;   // fluent API
    }

    List<String> validate(T value) {
        return rules.stream()
            .filter(rule -> !rule.check(value))
            .map(ValidationRule::errorMessage)
            .collect(Collectors.toList());
    }

    boolean isValid(T value) { return validate(value).isEmpty(); }
}

// Build a validator for email strings:
Validator<String> emailValidator = new Validator<String>()
    .require(Predicate.not(String::isBlank),        "Email cannot be blank")
    .require(s -> s.contains("@"),                  "Email must contain @")
    .require(s -> s.indexOf("@") > 0,               "Email must have local part")
    .require(s -> s.lastIndexOf(".") > s.indexOf("@"), "Email must have domain")
    .require(s -> s.length() <= 254,                "Email too long");

System.out.println(emailValidator.validate(""));
// [Email cannot be blank, Email must contain @, Email must have local part, ...]

System.out.println(emailValidator.validate("user@example.com"));
// []  (no errors — valid email)

// Reusable predicate building blocks:
Predicate<String> hasMinLength = s -> s.length() >= 8;
Predicate<String> hasUppercase = s -> s.chars().anyMatch(Character::isUpperCase);
Predicate<String> hasDigit     = s -> s.chars().anyMatch(Character::isDigit);
Predicate<String> hasSpecial   = s -> s.chars().anyMatch(c -> !Character.isLetterOrDigit(c));

Predicate<String> strongPassword = hasMinLength
    .and(hasUppercase)
    .and(hasDigit)
    .and(hasSpecial);

System.out.println(strongPassword.test("Secret1!"));   // true
System.out.println(strongPassword.test("password"));   // false (no upper, digit, special)

BiPredicate, Primitive Specializations, and Advanced Patterns

BiPredicate<T,U> is the two-argument generalization of Predicate, with the single abstract method boolean test(T t, U u). It represents any predicate that tests a relationship between two values: equality, comparison, containment, or any custom binary condition. BiPredicate has the same composition methods as Predicate — and(), or(), negate() — operating on both arguments simultaneously. Common uses: String::startsWith as BiPredicate<String,String> (does the first arg start with the second?), Objects::equals as BiPredicate<Object,Object>, or any custom two-value condition like a range check or a permission check. The primitive specializations IntPredicate, LongPredicate, and DoublePredicate test a single primitive value without boxing. Their abstract methods are boolean test(int value), boolean test(long value), and boolean test(double value). They have the same and(), or(), and negate() composition methods as Predicate. Using these in IntStream, LongStream, and DoubleStream pipelines avoids boxing overhead. The Pattern.asPredicate() and Pattern.asMatchPredicate() methods (Java 8 and 11+ respectively) create Predicates from compiled regular expressions. asPredicate() returns a predicate that tests whether any subsequence of the input matches the pattern (Matcher.find()). asMatchPredicate() returns a predicate that tests whether the entire input matches the pattern (Matcher.matches()). These allow regular expression matching to be integrated cleanly into stream pipelines and validation chains. A subtle but important pattern is using Predicate for early termination in custom iteration. When traversing data where you want to stop on the first match, combining Predicate with a loop that checks the predicate per element is cleaner than nested conditionals. More powerful: Stream.takeWhile(Predicate) (Java 9+) produces a stream of all leading elements that satisfy the predicate and stops at the first failure — perfect for processing sorted sequences up to a boundary condition.
Java
// ── BiPredicate<T,U> ────────────────────────────────────────────────
BiPredicate<String, String> startsWith = String::startsWith;
BiPredicate<String, String> endsWith   = String::endsWith;
BiPredicate<String, String> contains   = String::contains;
BiPredicate<Object, Object> equals     = Objects::equals;

System.out.println(startsWith.test("hello world", "hello"));  // true
System.out.println(endsWith.test("hello world", "world"));    // true
System.out.println(equals.test("foo", "foo"));                // true
System.out.println(equals.test("foo", null));                 // false

// Composition:
BiPredicate<String, String> startsAndEnds =
    startsWith.and((s, t) -> s.endsWith(t));  // same prefix = suffix

System.out.println(startsAndEnds.test("abcabc", "abc"));  // true
System.out.println(startsAndEnds.test("abcdef", "abc"));  // false

// Custom BiPredicate for range checking:
BiPredicate<Integer, Integer> withinRange = (value, limit) ->
    Math.abs(value) <= limit;
System.out.println(withinRange.test(5, 10));   // true
System.out.println(withinRange.test(15, 10));  // false

// ── IntPredicate, LongPredicate, DoublePredicate ──────────────────────
IntPredicate positive   = n -> n > 0;
IntPredicate even       = n -> n % 2 == 0;
IntPredicate positiveEven = positive.and(even);

System.out.println(positiveEven.test(4));   // true  (no boxing)
System.out.println(positiveEven.test(-4));  // false
System.out.println(positiveEven.test(3));   // false

// In IntStream pipeline — no boxing:
int[] numbers = IntStream.rangeClosed(-10, 10).toArray();
int[] filtered = Arrays.stream(numbers)
    .filter(positive.and(even))    // IntPredicate — no Integer boxing
    .toArray();
System.out.println(Arrays.toString(filtered));  // [2, 4, 6, 8, 10]

// LongPredicate for large values:
LongPredicate isLeapYear = year ->
    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
System.out.println(isLeapYear.test(2024L));  // true
System.out.println(isLeapYear.test(1900L));  // false (divisible by 100 but not 400)
System.out.println(isLeapYear.test(2000L));  // true (divisible by 400)

// DoublePredicate:
DoublePredicate isFinitePositive = d -> Double.isFinite(d) && d > 0;
System.out.println(isFinitePositive.test(3.14));            // true
System.out.println(isFinitePositive.test(Double.NaN));      // false
System.out.println(isFinitePositive.test(Double.POSITIVE_INFINITY));  // false

// ── Pattern.asPredicate() and asMatchPredicate() ──────────────────────
Pattern emailPattern = Pattern.compile("[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}");
Predicate<String> isValidEmail = emailPattern.asMatchPredicate();  // full match

List<String> candidates = List.of(
    "user@example.com", "invalid", "also@valid.org", "@nolocal.com", "no@"
);
List<String> validEmails = candidates.stream()
    .filter(isValidEmail)
    .collect(Collectors.toList());
System.out.println(validEmails);  // [user@example.com, also@valid.org]

// asPredicate vs asMatchPredicate:
Pattern digits = Pattern.compile("\d+");
Predicate<String> containsDigits = digits.asPredicate();        // find() — subsequence
Predicate<String> isAllDigits    = digits.asMatchPredicate();   // matches() — full string

System.out.println(containsDigits.test("abc123def"));  // true (contains digits)
System.out.println(isAllDigits.test("abc123def"));     // false (not all digits)
System.out.println(isAllDigits.test("12345"));         // true

// ── Stream.takeWhile(Predicate) — Java 9+ ────────────────────────────
List<Integer> sorted = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Take elements while they are < 5, stop at first failure:
List<Integer> leading = sorted.stream()
    .takeWhile(n -> n < 5)
    .collect(Collectors.toList());
System.out.println(leading);  // [1, 2, 3, 4]

// Stream.dropWhile(Predicate) — Java 9+ — complement:
List<Integer> trailing = sorted.stream()
    .dropWhile(n -> n < 5)
    .collect(Collectors.toList());
System.out.println(trailing);  // [5, 6, 7, 8, 9, 10]

// Practical: process log lines up to a section boundary:
List<String> logLines = Files.readAllLines(Path.of("app.log"));
List<String> untilError = logLines.stream()
    .takeWhile(Predicate.not(l -> l.startsWith("ERROR:")))
    .collect(Collectors.toList());

Related Topics in Java 8 Features

Lambda Expressions
Lambda expressions, introduced in Java 8, are anonymous functions — blocks of code that can be stored in variables, passed as arguments, and returned from methods, treating behavior as data. A lambda has three parts: a parameter list, an arrow token (->), and a body. The body is either a single expression (whose value is the implicit return value) or a block of statements wrapped in braces. Lambdas implement functional interfaces — interfaces with exactly one abstract method — allowing any lambda whose signature matches the abstract method's signature to be used wherever that interface is expected. The lambda syntax is syntactic sugar: every lambda is compiled to an invocation of the functional interface's abstract method, with the compiler generating a class (via invokedynamic) that implements the interface and delegates to the lambda body. This entry covers the complete lambda syntax including all shorthand forms, variable capture and the effectively-final constraint, method references as a specialized lambda syntax, the relationship between lambdas and the type system, how lambdas interact with exception handling, the invokedynamic compilation strategy and its performance characteristics, and the complete set of rules governing lambda type inference.
Functional Interfaces
A functional interface is any Java interface that has exactly one abstract method. This single-abstract-method (SAM) contract makes the interface a valid target type for a lambda expression or method reference — the lambda provides the implementation of that one abstract method. The @FunctionalInterface annotation is optional but strongly recommended: it causes the compiler to verify that the interface satisfies the SAM constraint, rejecting it at compile time if there is more than one abstract method. The java.util.function package, introduced in Java 8, provides 43 standard functional interfaces organized around four root types — Function, Consumer, Supplier, Predicate — and their variations for primitives (IntFunction, LongSupplier, DoubleConsumer, etc.), binary operations (BiFunction, BiConsumer, BiPredicate), and unary operators (UnaryOperator, IntUnaryOperator, etc.). This entry covers the design principles behind functional interfaces, the complete @FunctionalInterface contract including default and static methods, the full java.util.function hierarchy and the pattern that governs naming, creating custom functional interfaces with checked exceptions, composing functional interfaces via default methods, and the relationship between functional interfaces and the type system including the rules for lambda assignment and widening.
Function
Function<T,R> is a functional interface in java.util.function representing a function that accepts one argument of type T and produces a result of type R, with the single abstract method R apply(T t). It is the most general transformation interface in the standard library, used throughout the Streams API for mapping (Stream.map()), in Optional for value transformation (Optional.map(), Optional.flatMap()), and as a building block for more specialized functional interfaces. Function provides two default composition methods — andThen() and compose() — that create new functions by chaining two functions together, enabling functional pipeline construction without intermediate variables. The specializations cover all combinations of generic and primitive inputs and outputs: ToIntFunction, IntFunction, IntToLongFunction, and so on. UnaryOperator<T> extends Function<T,T> for operations that transform a value within the same type. BiFunction<T,U,R> generalizes to two input arguments. This entry covers the complete Function API, the semantics of andThen versus compose, all specializations and when each is appropriate, the functional relationship between Function and other java.util.function types, partial application patterns, and Function as the basis for building data pipelines.
Consumer
Consumer<T> is a functional interface in java.util.function that represents an operation that accepts a single input argument and returns no result. It is the standard abstraction for side-effect-oriented operations — printing, logging, persisting, modifying in-place, or any action that consumes a value without producing a new one. Consumer<T> declares one abstract method, accept(T t), and one default method, andThen(Consumer<? super T> after), which composes two consumers sequentially: the second consumer receives the same input as the first, enabling chaining of multiple side effects on the same value. The java.util.function package also provides primitive specializations — IntConsumer, LongConsumer, DoubleConsumer — that avoid boxing overhead, and BiConsumer<T, U> for operations on two arguments. Consumer is used throughout the Stream API (forEach), the Collections API (Iterable.forEach, Map.forEach), and is the standard type for callback and event-handler parameters. This entry covers the full Consumer API including andThen composition, all primitive specializations and BiConsumer, the distinction between Consumer and other functional interfaces, common usage patterns in streams and collections, and how to write effective Consumer implementations.