☕ 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

Intermediate Operations
Intermediate operations are Stream API methods that transform one stream into another stream, enabling pipeline construction through method chaining. Every intermediate operation is lazy — it does not process any elements when called; it only builds a description of the computation to be performed. The actual processing occurs only when a terminal operation triggers stream traversal, at which point all intermediate operations execute in a single pass over the data, interleaved element by element rather than stage by stage. This laziness and fusion model is the defining architectural feature of the Streams API, distinguishing it from eager collection-transformation approaches. Intermediate operations fall into categories: filtering (filter, distinct), transformation (map and its primitive variants, flatMap), ordering (sorted), size-limiting (limit, skip), peeking (peek), and the Java 9 additions for prefix-based selection (takeWhile, dropWhile). This entry covers the laziness model and why it matters, every intermediate operation with its exact semantics and performance characteristics, stateless versus stateful intermediate operations, short-circuiting operations and how they interact with infinite streams, the map/flatMap distinction, and operation ordering and its performance implications.
Terminal Operations
Terminal operations are Stream API methods that trigger the actual traversal and processing of a stream pipeline, producing a non-stream result — a value, a collection, a side effect, or nothing (void). A stream can have exactly one terminal operation; once invoked, the stream is consumed and cannot be reused (calling any further operation on it throws IllegalStateException). Terminal operations fall into categories: reduction (reduce, collect), matching (anyMatch, allMatch, noneMatch), finding (findFirst, findAny), counting (count), iteration with side effects (forEach, forEachOrdered), and array/collection materialization (toArray, toList). Some terminal operations are short-circuiting (anyMatch, findFirst) and can terminate before examining the entire stream; others (forEach, count in most cases, collect) must examine every element. This entry covers every terminal operation in depth, the reduce() method and its three overloads with the combiner function's role in parallel execution, the Collectors framework as the primary mechanism for complex terminal reduction, short-circuiting semantics and their interaction with infinite streams, the single-use constraint and its rationale, and the choice between equivalent terminal operations for performance and clarity.
Optional
Optional<T>, introduced in Java 8, is a container object that may or may not hold a non-null value, designed to make the possible absence of a value explicit in a method's type signature rather than implicit through a nullable return type. Optional.of(value) creates a present Optional and throws NullPointerException if value is null; Optional.empty() creates an absent Optional; Optional.ofNullable(value) creates either a present or empty Optional depending on whether value is null. The design intent, stated explicitly in the Javadoc, is as a return type for methods where the absence of a result is a valid and expected outcome, communicating that absence through the type system instead of through null, with the goal of reducing NullPointerException at the API boundary. Optional is explicitly not intended for use as a field type, a method parameter type, or inside collections, and the JDK team has stated that misuse in these contexts works against its design intent. This entry covers the complete Optional API including creation, value extraction, conditional execution, transformation and chaining, the primitive specializations OptionalInt/OptionalLong/OptionalDouble, and the established conventions for where Optional should and should not be used.
Default Methods
Default methods, introduced in Java 8, are methods declared in an interface with a body, marked with the default keyword, providing a concrete implementation that implementing classes inherit automatically without being forced to override it. Before Java 8, interfaces could declare only abstract methods, meaning adding any new method to an interface would break every existing implementation — a problem that became critical when the Streams API needed to add methods like forEach(), stream(), and removeIf() to the existing Collection-family interfaces without breaking the entire ecosystem of third-party collection implementations. Default methods solved this by allowing interface evolution: new methods can be added to an interface with a default implementation, and existing implementing classes continue to compile and work without modification, inheriting the default behavior until they choose to override it. This entry covers the design motivation and interface evolution problem default methods solve, the complete syntax and inheritance rules, the diamond problem and Java's resolution rules for conflicting default methods from multiple interfaces, the explicit disambiguation syntax using InterfaceName.super.method(), the interaction between default methods and abstract classes, and the static method companion feature introduced alongside default methods.