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() { ... }
}
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
}
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));
Practical Examples
1. Comparing Two Values Directly
BiPredicate<Integer, Integer> isGreater = (a, b) -> a > b;
System.out.println(isGreater.test(10, 5)); // true
2. Validating Related Fields
BiPredicate<LocalDate, LocalDate> isValidRange = LocalDate::isBefore;
boolean valid = isValidRange.test(order.getStartDate(), order.getEndDate());
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
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);
}
});
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,matchessay more than a generic check. -
Keep It Side-Effect Free: A
BiPredicateshould only answer true or false, never perform an action. -
Compose Small Predicates: Build complex relational checks out of
and/or/negaterather than one large lambda.
Common Pitfalls
-
Forcing It Where Predicate Fits: If the second value is just a fixed constant, a plain
Predicatewith a captured variable is simpler. -
No Direct Stream Integration:
Stream.filtertakes aPredicate, not aBiPredicate, 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)