Definition
- AND logic – combine
"IT".equals(e.department())ande.joinYear() > 2015with&&. - Chained filters –
filter(isIT).filter(joinedAfter2015)is logically identical to a single ANDed predicate. - Short-circuit – Java evaluates left to right and stops at the first false, skipping unnecessary checks.
- String comparison – always use
equals(), never==. Put the constant on the left for null safety. - Boundary – 'after 2015' means strictly greater than 2015; 2015 itself does not qualify.
The HR manager at National Bank of India has two rules at the door: 'You must be in the IT department AND you must have joined after 2015.' Rajesh joined IT in 2018 and walks through. Someone who joined IT in 2010 is stopped because he fails the second rule. Both rules must say yes.
Understanding the Concepts
How do you filter with two conditions?
A predicate is any function returning a boolean. To require two things at once, combine them with the logical AND operator.
package com.bank.service;
import java.util.List;
public class MultiConditionFilter {
record Employee(String name, String department, int joinYear, double salary) {}
public static void main(String[] args) {
List<Employee> staff = List.of(
new Employee("Rajesh", "IT", 2018, 85000),
new Employee("Priya", "IT", 2010, 72000),
new Employee("Amit", "Finance", 2019, 95000),
new Employee("Deepa", "IT", 2016, 110000),
new Employee("Vikram", "IT", 2015, 78000)
);
// Single lambda with &&
List<Employee> result = staff.stream()
.filter(e -> "IT".equals(e.department()) && e.joinYear() > 2015)
.toList();
result.forEach(e -> System.out.println(e.name() + " (" + e.joinYear() + ")"));
}
}
Rajesh (2018)
Deepa (2016)
Vikram joined IT in 2015, but 'after 2015' means strictly greater than, so he does not qualify.
How do you use Predicate combinators?
import java.util.function.Predicate;
Predicate<Employee> isIT = e -> "IT".equals(e.department());
Predicate<Employee> joinedAfter2015 = e -> e.joinYear() > 2015;
// Compose with and()
List<Employee> result = staff.stream()
.filter(isIT.and(joinedAfter2015))
.toList();
// Reads like English: "is IT and joined after 2015"
Named predicates are reusable, testable, and compose cleanly with and(), or(), and negate().
What is the difference between chained filters and a single AND?
| Approach | Code | Effect | |----------|------|--------| | Single AND | `filter(e -> A && B)` | One predicate, one filter call | | Chained filters | `filter(A).filter(B)` | Two filter calls, same result | | Predicate.and() | `filter(predA.and(predB))` | Composed, reusable |
All three produce the same output. Chained filters are slightly more readable for independent conditions. The single AND is compact for inline use.
Why must you use equals() for Strings?
// WRONG: reference comparison
staff.stream().filter(e -> e.department() == "IT"); // may fail for distinct String objects
// CORRECT: value comparison
staff.stream().filter(e -> "IT".equals(e.department())); // null-safe, always correct
Putting the constant "IT" on the left side means if e.department() is null, the call returns false instead of throwing NullPointerException.
What are common mistakes?
// WRONG: inclusive boundary when exclusive is required
staff.stream().filter(e -> e.joinYear() >= 2015); // includes 2015 itself
// CORRECT: strictly after 2015
staff.stream().filter(e -> e.joinYear() > 2015);
// WRONG: using == for String comparison
staff.stream().filter(e -> e.department() == "IT");
// CORRECT: using equals()
staff.stream().filter(e -> "IT".equals(e.department()));
How does short-circuiting help performance?
Java evaluates && left to right and stops at the first false. If most employees are not in IT, putting the department check first means the join-year check is skipped for the majority of records.
// Department check first: cheaper, more selective
staff.stream().filter(e -> "IT".equals(e.department()) && e.joinYear() > 2015);
What about using LocalDate for join dates?
import java.time.LocalDate;
record Employee(String name, String department, LocalDate joinDate) {}
LocalDate cutoff = LocalDate.of(2015, 12, 31);
staff.stream()
.filter(e -> "IT".equals(e.department()) && e.joinDate().isAfter(cutoff))
.toList();
In production banking code, use LocalDate instead of raw year integers for precise date handling.
What should a senior engineer know?
- Named predicates make business rules testable in isolation and reusable across services.
- In Spring Data, prefer
@Querywith WHERE clauses over in-memory filter for large employee tables. - The order of conditions affects performance due to short-circuiting: put the cheapest and most selective check first.
- For null-safety on department fields,
"IT".equals(e.department())is a defensive habit that prevents NPE.
Visual Overview
Multi-condition filter:
───────────────────────
Source: [Rajesh:IT:2018, Priya:IT:2010, Amit:Fin:2019, Deepa:IT:2016, Vikram:IT:2015]
│
filter(dept == IT && joinYear > 2015)
│
Rajesh:IT:2018 ✓ IT ✓ 2018>2015 → keep
Priya:IT:2010 ✓ IT ✗ 2010>2015 → discard
Amit:Fin:2019 ✗ Fin → discard (short-circuit)
Deepa:IT:2016 ✓ IT ✓ 2016>2015 → keep
Vikram:IT:2015 ✓ IT ✗ 2015>2015 → discard (not strictly after)
│
Result: [Rajesh, Deepa]
Key Takeaway
When two conditions must both hold, combine them with logical AND in a single predicate or chain two filter calls. Both are equivalent. Java short-circuits the AND left to right, so the second condition is skipped when the first is false.
The boundary detail matters: 'after 2015' means strictly greater than 2015, so someone who joined in 2015 does not qualify. Always compare Strings with equals(), put the constant on the left for null safety, and consider extracting named predicates for reusable, testable business rules.
In production banking code, use LocalDate instead of raw year integers for precise date handling, and prefer database WHERE clauses over in-memory filter for large datasets.
Remember one sentence: combine conditions with AND (chained filters or &&), use equals() for Strings, and be precise about boundary inclusivity.
---