Java Tutorial
🔍

Java Stream filter() Method

Java Stream filter() Method

filter() is the Stream intermediate operation that keeps only the elements matching a given condition and discards the rest, without ever touching the original source. It takes a Predicate<T> and returns a new Stream<T> containing just the elements that predicate returned true for. It is usually the very first call in a pipeline, since narrowing the data down early means every operation chained after it has less work left to do.

What Is filter()?

filter(Predicate<? super T> predicate) is declared directly on Stream<T>, taking a single Predicate and returning a new stream containing only the elements that satisfy it. Like every intermediate operation, it is lazy — building a pipeline that calls filter() does not evaluate the predicate against anything until a terminal operation actually pulls elements through the pipeline.

IntStream, LongStream, and DoubleStream each declare their own filter() overload too, accepting IntPredicate, LongPredicate, and DoublePredicate instead of a boxed Predicate<Integer>, Predicate<Long>, or Predicate<Double> — the same operation, specialized to avoid autoboxing.

Why filter() Was Introduced

Keeping only the elements matching a condition used to mean a loop, an if check, and a second list built up one element at a time to hold the survivors.

1// File: BeforeFilter.java 2import java.util.*; 3 4public class BeforeFilter { 5 record Candidate(String name, int experienceYears) {} 6 7 public static void main(String[] args) { 8 List<Candidate> candidates = List.of( 9 new Candidate("Ananya", 2), 10 new Candidate("Rohit", 5), 11 new Candidate("Priya", 6), 12 new Candidate("Vikram", 1) 13 ); 14 15 List<Candidate> shortlisted = new ArrayList<>(); 16 for (Candidate candidate : candidates) { 17 if (candidate.experienceYears() >= 4) { 18 shortlisted.add(candidate); 19 } 20 } 21 22 for (Candidate candidate : shortlisted) { 23 System.out.println(candidate.name()); 24 } 25 } 26}
Output:
Rohit
Priya

The same logic as filter() reads as a description of what should survive, with the condition itself as the only thing that changes from one use to the next.

1// File: AfterFilter.java 2import java.util.*; 3import java.util.stream.*; 4 5public class AfterFilter { 6 record Candidate(String name, int experienceYears) {} 7 8 public static void main(String[] args) { 9 List<Candidate> candidates = List.of( 10 new Candidate("Ananya", 2), 11 new Candidate("Rohit", 5), 12 new Candidate("Priya", 6), 13 new Candidate("Vikram", 1) 14 ); 15 16 candidates.stream() 17 .filter(candidate -> candidate.experienceYears() >= 4) 18 .forEach(candidate -> System.out.println(candidate.name())); 19 } 20}
Output:
Rohit
Priya

Both versions produce the same two names. The stream version has no separate shortlisted list to manage and no loop variable tracking progress through it.

Syntax

filter() works with a plain lambda, a method reference, or a Predicate variable, and it can be chained more than once in the same pipeline.

1// File: FilterSyntaxForms.java 2import java.util.*; 3import java.util.stream.*; 4 5public class FilterSyntaxForms { 6 public static void main(String[] args) { 7 List<String> skills = List.of("Java", "Python", "SQL", "Go", "JavaScript"); 8 9 // Basic filter with a lambda 10 List<String> longSkillNames = skills.stream() 11 .filter(skill -> skill.length() > 3) 12 .collect(Collectors.toList()); 13 14 // Chaining two filters - equivalent to combining both with AND 15 List<String> filteredTwice = skills.stream() 16 .filter(skill -> skill.length() > 3) 17 .filter(skill -> skill.startsWith("J")) 18 .collect(Collectors.toList()); 19 20 // filter on a primitive IntStream uses IntPredicate instead of Predicate<Integer> 21 int[] scores = {45, 78, 92, 60, 88}; 22 long passingCount = Arrays.stream(scores) 23 .filter(score -> score >= 60) 24 .count(); 25 26 System.out.println("Long skill names: " + longSkillNames); 27 System.out.println("Filtered twice: " + filteredTwice); 28 System.out.println("Passing count: " + passingCount); 29 } 30}
Output:
Long skill names: [Java, Python, JavaScript]
Filtered twice: [Java, JavaScript]
Passing count: 4

Common Use Cases

Combining Conditions With a Predicate

Passing an already-combined Predicate into filter() keeps a compound condition readable, rather than cramming several checks into one lambda.

1// File: CombinedFilterConditionExample.java 2import java.util.*; 3import java.util.function.*; 4import java.util.stream.*; 5 6public class CombinedFilterConditionExample { 7 record Candidate(String name, int experienceYears, String location) {} 8 9 public static void main(String[] args) { 10 List<Candidate> candidates = List.of( 11 new Candidate("Ananya", 4, "Bengaluru"), 12 new Candidate("Rohit", 6, "Pune"), 13 new Candidate("Priya", 3, "Bengaluru") 14 ); 15 16 Predicate<Candidate> minExperience = candidate -> candidate.experienceYears() >= 4; 17 Predicate<Candidate> inBengaluru = candidate -> candidate.location().equals("Bengaluru"); 18 19 List<String> matches = candidates.stream() 20 .filter(minExperience.and(inBengaluru)) 21 .map(Candidate::name) 22 .collect(Collectors.toList()); 23 24 System.out.println(matches); 25 } 26}
Output:
[Ananya]

filter() Versus removeIf()

filter() produces a brand new stream and leaves the original collection untouched. removeIf() does the opposite — it mutates the collection it is called on directly, in place.

1// File: FilterVsRemoveIfExample.java 2import java.util.*; 3 4public class FilterVsRemoveIfExample { 5 public static void main(String[] args) { 6 List<Integer> originalScores = new ArrayList<>(List.of(45, 78, 92, 60, 88)); 7 8 List<Integer> passingViaFilter = originalScores.stream() 9 .filter(score -> score >= 60) 10 .toList(); 11 12 System.out.println("Original after filter(): " + originalScores); 13 System.out.println("New list from filter(): " + passingViaFilter); 14 15 originalScores.removeIf(score -> score < 60); 16 System.out.println("Original after removeIf(): " + originalScores); 17 } 18}
Output:
Original after filter(): [45, 78, 92, 60, 88]
New list from filter(): [78, 92, 60, 88]
Original after removeIf(): [78, 92, 60, 88]

Filtering Alongside a Short-Circuiting Terminal Operation

findFirst() stops pulling elements through the pipeline the instant it finds a match, and combined with filter(), this means elements after the match are never even checked.

1// File: FilterShortCircuitExample.java 2import java.util.*; 3 4public class FilterShortCircuitExample { 5 public static void main(String[] args) { 6 List<String> candidateIds = List.of("C-1", "C-2", "C-3", "C-4", "C-5"); 7 8 Optional<String> firstMatch = candidateIds.stream() 9 .filter(id -> { 10 System.out.println("Checking " + id); 11 return id.equals("C-3"); 12 }) 13 .findFirst(); 14 15 System.out.println("Found: " + firstMatch.orElse("none")); 16 } 17}
Output:
Checking C-1
Checking C-2
Checking C-3
Found: C-3

Filtering With a Method Reference

A boolean-returning getter turns into a filter condition with no lambda body needed at all, using the same unbound method reference pattern covered earlier in this series.

1// File: FilterMethodReferenceExample.java 2import java.util.*; 3import java.util.stream.*; 4 5public class FilterMethodReferenceExample { 6 record JobPosting(String title, boolean isOpen) {} 7 8 public static void main(String[] args) { 9 List<JobPosting> postings = List.of( 10 new JobPosting("Backend Engineer", true), 11 new JobPosting("QA Lead", false), 12 new JobPosting("Frontend Engineer", true) 13 ); 14 15 List<String> openTitles = postings.stream() 16 .filter(JobPosting::isOpen) 17 .map(JobPosting::title) 18 .collect(Collectors.toList()); 19 20 System.out.println(openTitles); 21 } 22}
Output:
[Backend Engineer, Frontend Engineer]

Real-World Example

An applicant tracking system typically shortlists candidates against several independent screening criteria at once — a minimum years of experience, a required skill, a preferred location — and recruiting teams add or adjust these criteria often enough that hardcoding them into one long condition becomes a maintenance problem fast. Chaining several filter() calls, one per criterion, keeps each check readable on its own and easy to reorder or remove without touching the others.

1// File: Candidate.java 2import java.util.*; 3 4public record Candidate(String name, int experienceYears, List<String> skills, String location) {}
1// File: CandidateShortlistService.java 2import java.util.*; 3import java.util.stream.*; 4 5public class CandidateShortlistService { 6 7 public List<String> shortlist(List<Candidate> candidates, int minExperience, 8 String requiredSkill, String preferredLocation) { 9 return candidates.stream() 10 .filter(candidate -> candidate.experienceYears() >= minExperience) 11 .filter(candidate -> candidate.skills().contains(requiredSkill)) 12 .filter(candidate -> candidate.location().equals(preferredLocation)) 13 .map(Candidate::name) 14 .collect(Collectors.toList()); 15 } 16}
1// File: CandidateShortlistDemo.java 2import java.util.*; 3 4public class CandidateShortlistDemo { 5 public static void main(String[] args) { 6 List<Candidate> candidates = List.of( 7 new Candidate("Ananya", 4, List.of("Java", "SQL"), "Bengaluru"), 8 new Candidate("Rohit", 6, List.of("Java", "Spring"), "Pune"), 9 new Candidate("Priya", 5, List.of("Java", "Docker"), "Bengaluru"), 10 new Candidate("Vikram", 2, List.of("Java", "SQL"), "Bengaluru") 11 ); 12 13 CandidateShortlistService service = new CandidateShortlistService(); 14 15 List<String> shortlist = service.shortlist(candidates, 4, "Java", "Bengaluru"); 16 17 System.out.println("Shortlisted candidates: " + shortlist); 18 } 19}
Output:
Shortlisted candidates: [Ananya, Priya]

A mistake that appears often in fresher pull requests is putting the cheapest-looking check first instead of the one most likely to eliminate the most candidates. Ordering filter() calls so the strictest condition runs earliest, as shortlist does by checking experience before skills and location, keeps every filter after it working on a smaller list — a detail that barely matters with four test records and matters quite a lot once the candidate pool is real production scale.

Combining filter() With Other Features

filter() always takes a Predicate<T>, so everything the Predicate article covers — and(), or(), negate(), Predicate.not() — composes directly into a single filter() call, and chaining several separate filter() calls, as CandidateShortlistService does, behaves identically to one combined, and()-joined predicate. The choice between the two styles is purely about readability. filter() also pairs naturally with short-circuiting terminal operations — findFirst, findAny, anyMatch, allMatch, noneMatch — which is exactly where its laziness produces a measurable benefit rather than just being a description of how it is implemented.

Best Practices

Put the condition most likely to eliminate the most elements first in a chain of filters. Every operation after that first filter() only ever sees the elements that survived it, so eliminating the bulk of the data early keeps the rest of the pipeline doing less work.

Prefer several small, clearly-named filter() calls, or predicates combined with and(), over one filter() holding a long, compound boolean expression crammed into a single lambda. Both behave identically — the difference is entirely about how easily the next person reading the code can tell what each condition actually checks.

Reach for a short-circuiting terminal operation like findFirst() or anyMatch() instead of collecting a full filtered list when only existence or the first match actually matters. Building a complete filtered List just to check whether it is empty does more work than the question actually needed.

Keep the predicate passed to filter() free of side effects. How many times it runs, and in what order, depends entirely on the terminal operation at the end of the pipeline, and relying on filter's timing for anything beyond returning true or false leads to behavior that changes the moment the pipeline itself changes.

Common Mistakes

Filtering into a full list just to check whether anything matched wastes the work anyMatch() was built to avoid — it collects every matching element before answering a question that only needed a yes or no.

1// File: FilterInsteadOfAnyMatchMistake.java 2import java.util.*; 3import java.util.stream.*; 4 5public class FilterInsteadOfAnyMatchMistake { 6 public static void main(String[] args) { 7 List<Integer> experienceYears = List.of(2, 4, 6, 1, 3); 8 9 // Wasteful - collects every matching element just to check if any exist 10 boolean hasSeniorWasteful = !experienceYears.stream() 11 .filter(years -> years >= 5) 12 .collect(Collectors.toList()) 13 .isEmpty(); 14 15 // anyMatch stops at the first match and never builds a list at all 16 boolean hasSeniorEfficient = experienceYears.stream() 17 .anyMatch(years -> years >= 5); 18 19 System.out.println("Wasteful check: " + hasSeniorWasteful); 20 System.out.println("Efficient check: " + hasSeniorEfficient); 21 } 22}
Output:
Wasteful check: true
Efficient check: true

Expecting filter() to flag or reject bad data is a mismatch of intent. filter() silently drops whatever does not match — it never reports what was removed or why, which makes it the wrong tool the moment invalid data should actually be treated as an error.

1// File: FilterSilentlyDropsMistake.java 2import java.util.*; 3import java.util.stream.*; 4 5public class FilterSilentlyDropsMistake { 6 public static void main(String[] args) { 7 List<Integer> ages = List.of(25, -5, 30, 200, 40); 8 9 // filter silently drops invalid ages instead of flagging them as errors 10 List<Integer> validAges = ages.stream() 11 .filter(age -> age > 0 && age < 120) 12 .collect(Collectors.toList()); 13 14 System.out.println("Valid ages: " + validAges); 15 System.out.println("Two entries were silently dropped - filter never reports what it removed"); 16 } 17}
Output:
Valid ages: [25, 30, 40]
Two entries were silently dropped - filter never reports what it removed

Assuming filter() runs against the entire collection before the next stage in the pipeline begins is a common misreading of how streams actually work. A sequential stream generally pushes one element through the whole pipeline — filter, then map, then whatever comes after — before moving on to the next element, rather than filtering everything first and only then starting the next stage on the survivors.

Interview Questions

Q1. What does the filter() method do, and what is its exact signature?

filter(Predicate<? super T> predicate) is a Stream<T> intermediate operation that returns a new stream containing only the elements for which predicate.test(element) returns true. It is lazy, so nothing is actually evaluated until a terminal operation runs, and it never modifies the source the stream was built from. Interviewers often follow up by asking for the exact signature specifically to check whether a candidate knows it returns a Stream<T>, not a List<T> or a boolean.

Q2. Does filter() modify the original collection?

No. filter() reads from its source and produces a new stream — the collection the stream was created from is never touched. This is a deliberate design choice shared by every intermediate operation in the Stream API, and it is the key distinction from Collection.removeIf(), which mutates its target directly.

Q3. What is the difference between filter() and removeIf()?

filter() is a Stream operation that returns a new stream of matching elements, leaving the original collection unchanged. removeIf() is a Collection method that removes matching elements from the collection it is called on, in place, with no new collection produced at all. Choosing between them comes down to whether the original data should survive — filter() when it should, removeIf() when the collection itself needs to shrink.

Q4. Why does the order of chained filter() calls matter for performance but not for the final result?

Because filter() calls chained with logical AND semantics are commutative — a.filter(x).filter(y) and a.filter(y).filter(x) keep exactly the same final set of elements regardless of order. What changes is how much work happens along the way: putting the filter that eliminates the most elements first means every filter after it processes fewer elements, while putting it last means the more expensive or less selective checks run against a larger set than necessary.

Q5. How does filter() interact with short-circuiting terminal operations like findFirst()?

A short-circuiting terminal operation like findFirst() stops pulling elements through the pipeline the moment it has what it needs, which means filter()'s predicate only runs against as many elements as it takes to find the first match — not the entire source. This is a favorite follow-up in product-based interviews, since it tests whether a candidate understands laziness as something with a real, observable effect on how many elements actually get processed, not just a description in documentation.

Q6. If you need to know whether any element matches a condition, why is anyMatch() usually better than filter() combined with a size check?

anyMatch() is short-circuiting — it stops evaluating as soon as one match is found and never builds a collection at all. filter() followed by .collect(...) and a size or emptiness check has to build the entire filtered result first, doing strictly more work to answer a question that only ever needed a true or false.

FAQs

Can filter() be applied to a primitive stream like IntStream?

Yes. IntStream, LongStream, and DoubleStream each declare their own filter() overload accepting IntPredicate, LongPredicate, or DoublePredicate, which avoids the autoboxing cost that filtering a boxed Stream<Integer> would carry.

What happens if the predicate passed to filter() throws an exception?

The exception propagates immediately out of the terminal operation that triggered the pipeline, and processing stops right there — no further elements are checked, and any partial result collected so far is discarded. filter() provides no built-in error handling of its own.

Does filter() run before or after map() if map() is written first in the chain?

Operations run in the exact order they are written in the pipeline. If map() appears before filter(), every element gets transformed first, and the filtering condition then runs against the transformed values — reversing the order changes what the predicate actually sees.

Can I chain multiple filter() calls instead of using Predicate.and()?

Yes, and both approaches produce identical results. a.filter(x).filter(y) and a.filter(x.and(y)) keep exactly the same elements — the choice is purely stylistic, based on whether separate filter() calls or one combined predicate reads more clearly for a given pipeline.

Is filter() a terminal or intermediate operation?

Intermediate. It returns a new Stream, not a final result, and like every intermediate operation it does nothing on its own until a terminal operation like collect, forEach, or count actually runs the pipeline.

Does filter() guarantee the order of elements is preserved?

Yes, for a stream with a defined encounter order — which a stream built from an ordered source like a List has by default. filter() never reorders elements; it only decides which ones pass through, so the relative order of the surviving elements stays exactly as it was in the source.

Can filter() be used with a method reference instead of a lambda?

Yes, whenever the condition is a single existing boolean-returning method, like JobPosting::isOpen. filter() accepts any Predicate<T>, and a method reference is just a more compact way of supplying one, exactly as covered in the Method References article.

Summary

filter() does one job, cleanly: keep the elements that match, drop the rest, and never touch the original source while doing it. The predicate it takes can be a lambda, a method reference, or several predicates combined with and(), and chaining multiple filter() calls in a pipeline is just a readability choice, not a different behavior.

The habit worth carrying forward is ordering filters by how much they actually eliminate — strictest first — and reaching for a short-circuiting terminal operation like anyMatch() or findFirst() the moment a full filtered list was never really the goal, the way the candidate shortlisting example and the wasted-existence-check mistake both illustrate from opposite directions. map(), reduce(), and collect() build on exactly the same pipeline discipline from here.

What to Read Next