Definition
- filter(Predicate
) – intermediate, lazy operation that retains elements for which the predicate returns true. - BigDecimal for money –
salary.compareTo(new BigDecimal("50000")) > 0compares precisely; never usedouble > 50000. - Non-destructive – the source List is untouched; filter returns a new stream.
- Fusible – chained filters are merged internally so each element passes through all tests in a single traversal.
- Composable – named Predicate instances combine with
and(),or(), andnegate().
The HR manager at National Bank of India stands at the door with one rule: 'Only let through employees who earn more than Rs.50,000.' Rajesh earns Rs.85,000 and walks through. A junior clerk earning Rs.30,000 is stopped. filter is exactly that door with a yes-or-no rule.
Understanding the Concepts
What does filter do?
Stream.filter(Predicate is an intermediate operation. It tests every element against a boolean condition and keeps only those where the condition is true. It is lazy: nothing happens until a terminal operation like collect or toList triggers the pipeline.
How do you filter employees by salary?
package com.bank.service;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
public class SalaryFilterDemo {
record Employee(String name, String department, BigDecimal salary) {}
public static void main(String[] args) {
List<Employee> staff = List.of(
new Employee("Rajesh", "IT", new BigDecimal("85000")),
new Employee("Priya", "Finance", new BigDecimal("72000")),
new Employee("Amit", "IT", new BigDecimal("30000")),
new Employee("Deepa", "HR", new BigDecimal("50000")),
new Employee("Vikram", "Finance", new BigDecimal("110000"))
);
BigDecimal threshold = new BigDecimal("50000");
List<Employee> highEarners = staff.stream()
.filter(e -> e.salary().compareTo(threshold) > 0)
.toList();
highEarners.forEach(e ->
System.out.println(e.name() + ": Rs." + e.salary()));
}
}
Rajesh: Rs.85000
Priya: Rs.72000
Vikram: Rs.110000
Deep earns exactly Rs.50,000 but the condition is strictly greater than, so she does not qualify. This boundary detail matters in interviews.
Why must you use BigDecimal for money?
// WRONG: double loses precision
double salary = 50000.01;
double threshold = 50000.00;
System.out.println(salary > threshold); // may give wrong answer for edge values
// CORRECT: BigDecimal compares exactly
BigDecimal salary = new BigDecimal("50000.01");
BigDecimal threshold = new BigDecimal("50000.00");
System.out.println(salary.compareTo(threshold) > 0); // true, always correct
BigDecimal stores the exact decimal value. A bank can never afford a rounding error on salary or balance.
How does filter differ from a for-loop?
| Aspect | filter | for-loop |
|--------|--------|----------|
| Style | Declarative | Imperative |
| Mutability | Source untouched | Often mutates a result list |
| Laziness | Lazy (deferred) | Eager (runs immediately) |
| Parallelism | Built-in via parallelStream | Manual threading |
| Readability | Pipeline reads left to right | Loop body can be complex |
// Imperative for-loop equivalent
List<Employee> result = new ArrayList<>();
for (Employee e : staff) {
if (e.salary().compareTo(threshold) > 0) {
result.add(e);
}
}
How do you compose predicates for reusable rules?
import java.util.function.Predicate;
Predicate<Employee> highSalary = e -> e.salary().compareTo(new BigDecimal("50000")) > 0;
Predicate<Employee> itDept = e -> "IT".equals(e.department());
// Combine with and()
List<Employee> highEarningIT = staff.stream()
.filter(highSalary.and(itDept))
.toList();
// [Rajesh: Rs.85000]
// Negate: employees NOT earning high salary
List<Employee> lowEarners = staff.stream()
.filter(highSalary.negate())
.toList();
What are common mistakes?
// WRONG: using == for String comparison
staff.stream().filter(e -> e.department() == "IT"); // reference comparison, may fail
// CORRECT: using equals
staff.stream().filter(e -> "IT".equals(e.department())); // null-safe, value comparison
// WRONG: using double for money
staff.stream().filter(e -> e.getSalaryDouble() > 50000.0); // floating-point imprecision
// CORRECT: using BigDecimal
staff.stream().filter(e -> e.salary().compareTo(threshold) > 0);
What are edge cases?
- Exactly on the boundary: Rs.50,000 does not pass
> 0comparison. Use>= 0for inclusive. - Empty stream: filter on an empty list produces an empty result, no exception.
- Null fields: if
salarycan be null, guard withe.salary() != null &&before compareTo. - All match: if every employee earns above 50,000, the result equals the original list (but as a new collection).
- None match: if no employee earns above 50,000, the result is an empty list.
What about parallel streams?
filter is stateless and thread-safe. Each element is tested independently with no shared mutable state.
List<Employee> highEarners = staff.parallelStream()
.filter(e -> e.salary().compareTo(threshold) > 0)
.toList(); // thread-safe, order preserved by toList
What should a senior engineer know?
- filter does not modify the original list. The source List
stays authoritative. - Chained filters are fused internally:
stream.filter(A).filter(B)tests each element against A and B in one pass, not two separate passes. - For large datasets, put the most selective predicate first so short-circuiting skips more elements.
- In Spring Data, prefer database-level WHERE clauses over in-memory filter for large result sets.
Visual Overview
filter pipeline:
────────────────
Source: [Rajesh:85K, Priya:72K, Amit:30K, Deepa:50K, Vikram:110K]
│
filter(salary > 50000)
│
Rajesh:85K ✓ keep
Priya:72K ✓ keep
Amit:30K ✗ discard
Deepa:50K ✗ discard (NOT strictly greater)
Vikram:110K ✓ keep
│
Result: [Rajesh:85K, Priya:72K, Vikram:110K]
Key Takeaway
Filtering a stream means keeping only the elements that satisfy a condition and discarding the rest. filter takes a Predicate and tests every element. Only those returning true survive into the result, which you materialise with collect or toList.
For salary comparisons in banking, always use BigDecimal and its compareTo method. Double and float lose precision, and even a tiny rounding error is unacceptable for money. The boundary detail matters: strictly greater than 50,000 excludes exactly 50,000.
filter is lazy, non-destructive, and fusible. It builds a pipeline that does nothing until a terminal operation runs, never modifies the original list, and fuses chained filters so each element is examined exactly once. Named predicates with and(), or(), and negate() make complex business rules readable and testable.
Remember one sentence: filter keeps elements where the predicate is true, and for money always use BigDecimal.compareTo, never double.
---