DEV Community

Cover image for Surfing with FP Java - Mastering BiPredicate<T, U>
André Borba
André Borba

Posted on

Surfing with FP Java - Mastering BiPredicate<T, U>

Introduction

In the last episode, BiFunction combined two inputs into a transformed result.

Now, let's return to Predicate's territory, boolean logic, but this time evaluating two values together with BiPredicate.

If Predicate asks a question about one value, BiPredicate asks a question about a relationship between two.

What Is BiPredicate?

@FunctionalInterface
public interface BiPredicate<T, U> {

    boolean test(T t, U u);

    default BiPredicate<T, U> and(BiPredicate<? super T, ? super U> other) { ... }
    default BiPredicate<T, U> or(BiPredicate<? super T, ? super U> other) { ... }
    default BiPredicate<T, U> negate() { ... }
}
Enter fullscreen mode Exit fullscreen mode

Input: two objects, of types T and U.
Output: a boolean.
Purpose: express a condition that depends on two values at once, not just one.

Why Use BiPredicate?

Before, relational checks between two values were usually buried inside an if statement:

if (startDate.isBefore(endDate)) {
    // valid range
}
Enter fullscreen mode Exit fullscreen mode

BiPredicate promotes that check into a reusable, named, composable value:

BiPredicate<LocalDate, LocalDate> isValidRange = (start, end) -> start.isBefore(end);

System.out.println(isValidRange.test(startDate, endDate));
Enter fullscreen mode Exit fullscreen mode

Practical Examples

1. Comparing Two Values Directly

BiPredicate<Integer, Integer> isGreater = (a, b) -> a > b;

System.out.println(isGreater.test(10, 5)); // true
Enter fullscreen mode Exit fullscreen mode

2. Validating Related Fields

BiPredicate<LocalDate, LocalDate> isValidRange = LocalDate::isBefore;

boolean valid = isValidRange.test(order.getStartDate(), order.getEndDate());
Enter fullscreen mode Exit fullscreen mode

3. Composition with and / or / negate

BiPredicate<String, String> sameLength = (a, b) -> a.length() == b.length();
BiPredicate<String, String> sameFirstChar = (a, b) -> a.charAt(0) == b.charAt(0);

BiPredicate<String, String> looksSimilar = sameLength.and(sameFirstChar);

System.out.println(looksSimilar.test("Borba", "Bruno")); // true
Enter fullscreen mode Exit fullscreen mode

4. Matching Pairs While Filtering Manually

BiPredicate<String, Integer> stockIsLow = (item, quantity) -> quantity < 5;

stock.forEach((item, quantity) -> {
    if (stockIsLow.test(item, quantity)) {
        System.out.println("Low stock: " + item);
    }
});
Enter fullscreen mode Exit fullscreen mode

Real-World Patterns

  • Field Validation: Checking that two related fields, like a date range or a min/max pair, are consistent with each other.
  • Matching Logic: Comparing two records to decide if they represent a match or a duplicate.
  • Filtering Map Entries: Combining a key and a value manually where Stream's single-argument filter doesn't apply directly.
  • Authorization Checks: Testing a user against a resource, e.g. BiPredicate<User, Document> canAccess.

Best Practices

  • Name for the Relationship: isValidRange, canAccess, matches say more than a generic check.
  • Keep It Side-Effect Free: A BiPredicate should only answer true or false, never perform an action.
  • Compose Small Predicates: Build complex relational checks out of and / or / negate rather than one large lambda.

Common Pitfalls

  • Forcing It Where Predicate Fits: If the second value is just a fixed constant, a plain Predicate with a captured variable is simpler.
  • No Direct Stream Integration: Stream.filter takes a Predicate, not a BiPredicate, adapting one to the other adds a layer of indirection worth naming clearly.
  • Deep Nesting: Combining many BiPredicates can hide the actual rule being tested, extract intermediate named predicates.

Functional Analogy

Think of BiPredicate as a judge in a duel:

  • Two contestants step forward, T and U.
  • The judge studies them together.
  • A single verdict comes back: true or false.

Conclusion

BiPredicate extends predicate logic into the space between two values, essential for validation, matching, and relational rules that a single-argument Predicate can't express.

What's Next

In our final stop, we'll meet BiConsumer, an interface that performs an action with two inputs and no result, closing our tour of Java's core functional interfaces.

Top comments (0)