Java Stream filter(): Filter Collections with Lambdas
- filter() keeps only elements where the predicate returns true β it's lazy and doesn't execute until a terminal operation like collect() or count() is called.
- Chain multiple filter() calls or combine conditions with && and || β multiple filter() calls are more readable for complex conditions.
- filter(Objects::nonNull) is the standard idiom for removing nulls from a stream before further processing.
filter() is the building block of data processing in modern Java. The power comes not from filter() alone but from composing it with map(), flatMap(), sorted(), and collect() in a readable declarative pipeline that describes what you want, not how to iterate to get it.
filter() Basics, Multiple Conditions, and Chaining
package io.thecodeforge.collections; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; public class StreamFilterExample { enum PaymentStatus { PENDING, COMPLETED, FAILED, REFUNDED } record Payment(String id, String customerId, int amountPence, PaymentStatus status) {} public static void main(String[] args) { List<Payment> payments = List.of( new Payment("p1", "cust-1", 100_00, PaymentStatus.COMPLETED), new Payment("p2", "cust-2", 50_00, PaymentStatus.FAILED), new Payment("p3", "cust-1", 250_00, PaymentStatus.COMPLETED), new Payment("p4", "cust-3", 75_00, PaymentStatus.PENDING), new Payment("p5", "cust-1", 10_00, PaymentStatus.REFUNDED) ); // Basic filter β single condition List<Payment> completed = payments.stream() .filter(p -> p.status() == PaymentStatus.COMPLETED) .collect(Collectors.toList()); System.out.println("Completed: " + completed.size()); // 2 // Multiple conditions β AND with && List<Payment> largCompletedForCust1 = payments.stream() .filter(p -> p.status() == PaymentStatus.COMPLETED) .filter(p -> p.customerId().equals("cust-1")) .filter(p -> p.amountPence() > 100_00) .collect(Collectors.toList()); System.out.println("Large completed for cust-1: " + largCompletedForCust1.size()); // 1 // Chain multiple filters as one predicate with && List<Payment> same = payments.stream() .filter(p -> p.status() == PaymentStatus.COMPLETED && p.customerId().equals("cust-1") && p.amountPence() > 100_00) .collect(Collectors.toList()); // filter + map + collect β the classic pipeline List<String> completedIds = payments.stream() .filter(p -> p.status() == PaymentStatus.COMPLETED) .map(Payment::id) .collect(Collectors.toList()); System.out.println("Completed IDs: " + completedIds); // [p1, p3] // Null-safe filter List<String> ids = List.of("p1", null, "p3", null, "p5"); List<String> nonNull = ids.stream() .filter(Objects::nonNull) .collect(Collectors.toList()); System.out.println("Non-null: " + nonNull); // [p1, p3, p5] // Count without collecting long failedCount = payments.stream() .filter(p -> p.status() == PaymentStatus.FAILED) .count(); System.out.println("Failed count: " + failedCount); // 1 } }
Large completed for cust-1: 1
Completed IDs: [p1, p3]
Non-null: [p1, p3, p5]
Failed count: 1
| Operation | Method | Example |
|---|---|---|
| Keep matching elements | filter(predicate) | .filter(p -> p.amount() > 100) |
| Negate condition | filter(predicate.negate()) | .filter(Predicate.not(String::isEmpty)) |
| Null-safe filter | filter(Objects::nonNull) | .filter(Objects::nonNull) |
| Multiple conditions AND | filter with && | .filter(p -> a && b) |
| Multiple conditions OR | filter with \|\| | .filter(p -> a \|\| b) |
| Combine predicates | Predicate.and/or() | p1.and(p2) |
π― Key Takeaways
- filter() keeps only elements where the predicate returns true β it's lazy and doesn't execute until a terminal operation like collect() or count() is called.
- Chain multiple filter() calls or combine conditions with && and || β multiple filter() calls are more readable for complex conditions.
- filter(Objects::nonNull) is the standard idiom for removing nulls from a stream before further processing.
- For boolean checks (does any element match? do all elements match?), use anyMatch(), allMatch(), or noneMatch() instead of filter().count() > 0.
β Common Mistakes to Avoid
- βCalling filter() on a stream with potential null elements without filtering nulls first β operations inside filter() lambdas will throw NPE if called on null elements.
- βForgetting filter() is lazy β it doesn't execute until a terminal operation is called. Debugging filter() without a terminal op will show no filtering happening.
- βMutating external state inside filter() β filter() is a pure function operation. Side effects inside lambdas are unpredictable in parallel streams.
- βUsing filter().findFirst() when anyMatch() suffices β if you only need to know whether any element matches, anyMatch() is cleaner and short-circuits earlier.
Interview Questions on This Topic
- QWhat is the difference between Stream.filter() and Stream.anyMatch()?
- QHow would you filter a List<Payment> to only include completed payments over Β£100 using Java streams?
- QIs Stream.filter() eager or lazy? What are the implications?
Frequently Asked Questions
How do I filter a list in Java 8+?
Use Stream.filter(): list.stream().filter(element -> condition).collect(Collectors.toList()). The filter() method takes a Predicate (a function returning boolean) and keeps only elements where the predicate returns true.
How do I filter null values from a Java stream?
Use filter(Objects::nonNull): stream.filter(Objects::nonNull).collect(Collectors.toList()). This removes all null elements before downstream operations, preventing NullPointerException in subsequent map() or other operations.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.