Senior 3 min · March 30, 2026

Java Stream filter() — Avoid NPE from Nulls

Production NPE in filter() from null element in stream source.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • filter() keeps elements where a Predicate returns true; it's lazy and doesn't run until a terminal op like collect() or count()
  • Combine multiple filter() calls or use && for AND, || for OR — but && is more readable for complex conditions
  • filter(Objects::nonNull) is the standard null-safe filter before downstream ops
  • Multiple filter() calls internally merge into one operation — no performance penalty from chaining
  • filter().findFirst() short-circuits; filter().count() does not — understand the difference for streaming large datasets
  • Biggest mistake: forgetting filter() is lazy — debugging filter() without a terminal op shows no filtering
Plain-English First

Stream.filter() is the Java streams version of 'keep only the elements that match this condition'. You pass a Predicate — a function that returns true or false for each element — and filter() keeps only the true ones. It's lazy: the filtering doesn't execute until a terminal operation (collect, count, findFirst) is called.

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 — The Predicate and Lazy Execution

Stream.filter() takes a Predicate<T> — a functional interface with a single test(T) method returning boolean. Every element is tested; only those where test returns true survive. filter() is an intermediate operation: it doesn't execute until a terminal operation like collect(), forEach(), or findFirst() is called.

This means you can chain multiple intermediate ops (filter, map, sorted) and the whole pipeline runs in one pass when the terminal op is invoked. The JVM may also fuse adjacent filter() calls into a single predicate internally for performance.

FilterBasics.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package io.thecodeforge.streams;

import java.util.List;
import java.util.stream.Collectors;

public class FilterBasics {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
        // filter() + collect() triggers execution
        List<Integer> evens = numbers.stream()
            .filter(n -> n % 2 == 0)
            .collect(Collectors.toList());
        System.out.println(evens); // [2, 4, 6]

        // Without terminal op — nothing prints
        numbers.stream()
            .filter(n -> {
                System.out.println("Testing " + n);
                return n % 2 == 0;
            });
        // No output because filter() is lazy
    }
}
Output
[2, 4, 6]
(No output from second stream)
Lazy Evaluation Trap
A common debugging mistake: adding System.out.println inside filter() and not seeing output. Remember, nothing executes until a terminal operation is called.
Production Insight
Lazy evaluation means filter() alone never throws — only when a terminal op triggers the pipeline.
In production, a pipeline that never terminates silently does no filtering, wasting CPU cycles on constructing the stream object without performing work.
Always add a terminal call, even if you discard the result (e.g., .count() or .allMatch(...)) to ensure the stream runs.
Key Takeaway
filter() defines what to keep; a terminal operation decides when to keep it.
Without a terminal operation, filter() does nothing.
Always terminate your stream.

Multiple Conditions: Chaining vs. Single Predicate

You can filter on multiple conditions either by chaining multiple filter() calls or by combining conditions with && inside one filter(). Both produce the same result, but readability and performance differ slightly.

Multiple filter() calls are often more readable — each expresses a single concern. The stream library is smart enough to fuse them into a single predicate internally. However, if one condition is much more selective than others, ordering matters: filter out the rarest condition first to reduce elements flowing through subsequent filters.

MultipleConditions.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package io.thecodeforge.streams;

import java.util.List;
import java.util.stream.Collectors;

record Payment(String id, String customerId, int amount, String status) {}

public class MultipleConditions {
    public static void main(String[] args) {
        List<Payment> payments = List.of(
            new Payment("p1", "cust-1", 100, "COMPLETED"),
            new Payment("p2", "cust-2", 50, "FAILED"),
            new Payment("p3", "cust-1", 250, "COMPLETED")
        );
        // Chained filters — each one independent
        List<Payment> chained = payments.stream()
            .filter(p -> "COMPLETED".equals(p.status()))
            .filter(p -> p.amount() > 75)
            .collect(Collectors.toList());
        System.out.println("Chained: " + chained.size()); // 1

        // Single filter with &&
        List<Payment> combined = payments.stream()
            .filter(p -> "COMPLETED".equals(p.status()) && p.amount() > 75)
            .collect(Collectors.toList());
        System.out.println("Combined: " + combined.size()); // 1
    }
}
Output
Chained: 1
Combined: 1
Production Insight
Multiple filter() calls vs. a single && predicate — performance is nearly identical because the JIT compiler can inline both.
However, ordering matters: if one condition is cheap and highly selective (e.g., filtering out 90% of elements), put it first. This reduces the number of elements the more expensive condition must evaluate.
In production code, prefer multiple filter() calls when conditions are conceptually separate; it improves readability and makes unit testing each condition easier.
Key Takeaway
Multiple filter() calls are fused by the JVM; readability wins.
Place the most selective condition first to minimise work.
Don't micro-optimise without profiling.
When to Chain vs Combine
IfConditions are independent and conceptually distinct
UseUse multiple filter() calls for readability and easier debugging
IfConditions are closely related and always evaluated together
UseUse a single filter() with && for conciseness
IfPerformance-critical path with millions of elements
UseProfile both approaches; ordering matters more than style

Predicate Composition: and(), or(), negate()

Java 8 introduced Predicate methods for composition: and(), or(), and negate(). These let you build complex filters without deep nesting of && and ||, especially when predicates are stored as variables or reused.

Use Predicate.not() (Java 11+) for negation. Combine predicates with .and() and .or() for more expressive pipeline construction.

PredicateComposition.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package io.thecodeforge.streams;

import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class PredicateComposition {
    public static void main(String[] args) {
        List<String> words = List.of("cat", "dog", "elephant", "ant", "");
        
        Predicate<String> notEmpty = s -> !s.isEmpty();
        Predicate<String> hasVowel = s -> s.matches(".*[aeiou].*");
        Predicate<String> longerThan3 = s -> s.length() > 3;

        // Use .and() to combine
        List<String> result = words.stream()
            .filter(notEmpty.and(hasVowel).and(longerThan3))
            .collect(Collectors.toList());
        System.out.println(result); // [elephant]

        // Use .or() with negate
        Predicate<String> noVowel = Predicate.not(hasVowel);
        List<String> noVowels = words.stream()
            .filter(notEmpty.and(noVowel))
            .collect(Collectors.toList());
        System.out.println(noVowels); // [] because all words have vowels
    }
}
Output
[elephant]
[]
Production Insight
Predicate composition is useful when the same filter condition appears in multiple places — extract it to a named Predicate constant and compose. However, be cautious with .and() and .or() in parallel streams: if the predicates share mutable state, you'll get race conditions. Always keep predicates stateless.
In production, heavy predicate composition with many .and() calls can obscure the logic. A helper method returning a Predicate can improve readability.
Key Takeaway
Use Predicate.and(), .or(), and .negate() to compose filters.
Extract reusable predicates for DRY code.
Keep predicates stateless for parallel safety.

Null-Safe Filtering and Optional Handling

Null values in a stream source are a common source of NPE in filter() predicates. The safest pattern is to filter out nulls with filter(Objects::nonNull) immediately after the stream creation. However, if you need to keep nulls for some reason (rare), use inline null checks in the predicate: p -> p != null && condition.

For scenarios where you have a stream of Optionals (e.g., map() returning Optional), use flatMap(Optional::stream) (Java 9+) to unwrap and filter out empties in one step.

NullSafeFilter.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package io.thecodeforge.streams;

import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

public class NullSafeFilter {
    public static void main(String[] args) {
        List<String> items = List.of("apple", null, "banana", null, "pear");
        
        // Standard null filter
        List<String> nonNull = items.stream()
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
        System.out.println(nonNull); // [apple, banana, pear]

        // If you need to transform and filter optional results
        List<String> parsed = items.stream()
            .filter(Objects::nonNull)
            .map(s -> Optional.of(s.toUpperCase()))
            .flatMap(Optional::stream)
            .collect(Collectors.toList());
        System.out.println(parsed); // [APPLE, BANANA, PEAR]
    }
}
Output
[apple, banana, pear]
[APPLE, BANANA, PEAR]
Null filter before method calls
filter(Objects::nonNull) must come before any filter that calls methods on the element. If you swap the order, the method-calling predicate throws NPE on nulls.
Production Insight
In production, trusting that data sources never produce null is dangerous. A null can slip in from deserialization of malformed JSON, a database migration, or a bug in a previous step. Adding filter(Objects::nonNull) at the top of every pipeline that performs method calls is a defensive pattern that costs almost nothing — microseconds per million elements — and prevents midnight NPE alerts.
Key Takeaway
filter(Objects::nonNull) first, then call methods.
Defensive null filtering is a one-liner that prevents production outages.
Use Optional::stream with flatMap for filtering alongside mapping.

Performance: Short-Circuiting, Ordering, and Large Datasets

filter() performance depends on the predicate cost, the number of elements, and whether you use short-circuiting operations like findFirst(), anyMatch(), or limit(). These terminate early as soon as a match is found, potentially processing only a fraction of the stream.

For large datasets (millions of elements), the cost of the predicate dominates. Putting an inexpensive, selective predicate first can drastically reduce the number of elements evaluated by downstream filters. Use parallelStream() only if the predicate is stateless, the stream source is large, and the operation is CPU-intensive — otherwise parallel overhead outweighs the benefit.

ShortCircuitPerformance.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package io.thecodeforge.streams;

import java.util.stream.IntStream;

public class ShortCircuitPerformance {
    public static void main(String[] args) {
        // findFirst short-circuits — may only inspect first few elements
        int firstEven = IntStream.range(1, 10_000_000)
            .filter(n -> {
                System.out.println("Checking " + n);
                return n % 2 == 0;
            })
            .findFirst()
            .orElse(-1);
        System.out.println("First even: " + firstEven); 
        // prints only "Checking 1" then "Checking 2" and stops
        
        // count() must process all elements — no short-circuit
        long count = IntStream.range(1, 10_000_000)
            .filter(n -> n % 2 == 0)
            .count();
        System.out.println("Count: " + count); // processes all 10M
    }
}
Output
Checking 1
Checking 2
First even: 2
Count: 5000000
Production Insight
Short-circuiting can dramatically reduce latency. For example, checking if any order is overdue for an alert: findFirst() stops after the first match, while filter().count() > 0 processes all orders. Use anyMatch() instead of filter().findFirst().isPresent() — it's more idiomatic and signals intent.
For batch processing of large datasets, avoid short-circuiting if you need to process everything. Instead, consider partitioning the stream with limit() and skip() for pagination, but be aware that skip() on unordered streams may not be deterministic — use order-dependent pipelines with caution.
Key Takeaway
Short-circuit ops (findFirst, anyMatch, limit) stop early — great for existence checks.
Expensive predicates first? Optimise ordering via profiling.
parallelStream() helps only if predicates are stateless and work outweighs overhead.
● Production incidentPOST-MORTEMseverity: high

NullPointerException in filter() predicate because upstream elements contained nulls

Symptom
Stack trace in production logs pointing to a lambda inside .filter(p -> p.getStatus() == Status.COMPLETED). The stream source List<Payment> contained a null entry.
Assumption
The team assumed the list would never contain null entries because the database column was NOT NULL. But a data migration created a new row with only the ID populated; the object was partially constructed and null slipped in.
Root cause
The stream pipeline didn't filter nulls before calling methods on elements. filter() passes every element to the predicate, including nulls. If the predicate invokes a method on the element, NPE occurs.
Fix
Insert filter(Objects::nonNull) before any filter() that accesses element methods. Also, enforce non-null contract at the data layer and validate object construction.
Key lesson
  • Always assume a stream source can contain nulls until proven otherwise — filter(Objects::nonNull) at the start of the pipeline is cheap insurance.
  • Even if the database column is NOT NULL, object mapping or deserialization can introduce nulls. Validate at the earliest point.
Production debug guideSymptom → Action — what to check when filter() isn't working as expected4 entries
Symptom · 01
filter() appears to do nothing — stream returns all elements
Fix
Check that a terminal operation (collect, count, findFirst) is called. Without it, filter() never executes. Add a .count() or .collect() at the end.
Symptom · 02
Predicate throws NullPointerException
Fix
Inspect the stream source for null elements. Insert filter(Objects::nonNull) before the predicate that accesses methods. Use peek() or debug logging to see the elements.
Symptom · 03
filter() returns fewer elements than expected
Fix
Check if conditions are too strict. Test the predicate manually on sample input. Use method references or Predicate.negate() to verify logic.
Symptom · 04
filter() with parallel stream produces inconsistent results
Fix
Ensure the predicate is stateless and non-interfering. Side effects (modifying external variables) break parallelism. Convert to sequential if uncertain.
★ Quick Debugging filter()Commands and checks when filter() behaves unexpectedly
No terminal operation -> nothing happens
Immediate action
Add .collect(Collectors.toList()) or .count() to force execution
Commands
list.stream().filter(p -> ...).collect(Collectors.toList())
list.stream().filter(p -> ...).peek(System.out::println).collect(toList())
Fix now
Always end pipeline with collect(), forEach(), or count() to trigger lazy evaluation
NullPointerException in predicate+
Immediate action
Insert filter(Objects::nonNull) before the failing filter
Commands
stream.filter(Objects::nonNull).filter(p -> p.getStatus() == ...)
stream.filter(p -> p != null && p.getStatus() == ...) // alternative inline null check
Fix now
Add null-safe filter as the first step in the pipeline
Parallel stream misbehavior+
Immediate action
Switch to sequential stream or ensure predicate is stateless
Commands
stream.sequential().filter(...).collect(...)
// check predicate for shared mutable state - remove it
Fix now
Parallel streams require thread-safe, stateless predicates
Stream filter() Operations at a Glance
OperationMethodExample
Keep matching elementsfilter(predicate).filter(p -> p.amount() > 100)
Negate conditionfilter(predicate.negate()).filter(Predicate.not(String::isEmpty))
Null-safe filterfilter(Objects::nonNull).filter(Objects::nonNull)
Multiple conditions ANDfilter with &&.filter(p -> a && b)
Multiple conditions ORfilter with \|\|.filter(p -> a \|\| b)
Combine predicatesPredicate.and/or()p1.and(p2)

Key takeaways

1
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.
2
Chain multiple filter() calls or combine conditions with && and ||
multiple filter() calls are more readable for complex conditions.
3
filter(Objects::nonNull) is the standard idiom for removing nulls from a stream before further processing.
4
For boolean checks (does any element match? do all elements match?), use anyMatch(), allMatch(), or noneMatch() instead of filter().count() > 0.
5
Short-circuiting operations (findFirst, limit) can dramatically reduce processing for large datasets when you only need a subset.
6
Predicate composition via and(), or(), negate() promotes reuse
but keep predicates stateless for parallel safety.

Common mistakes to avoid

5 patterns
×

Calling filter() on a stream with potential null elements without filtering nulls first

Symptom
NullPointerException inside filter() predicate when trying to access methods on null elements — production incidents at 3 AM.
Fix
Always add filter(Objects::nonNull) as the first operation after stream() if the source might contain nulls. Alternatively, use inline null check: p -> p != null && condition.
×

Forgetting filter() is lazy — debugging filter() without a terminal operation

Symptom
System.out.println inside filter() produces no output; developer thinks the condition is wrong, but actually the pipeline never executed.
Fix
Add a terminal operation like collect(), forEach(), or count() to trigger execution. Use .peek() for debugging, which is also lazy until terminal op.
×

Mutating external state inside filter() predicate

Symptom
Inconsistent results when switching to parallelStream() — shared mutable state causes race conditions, output varies per run.
Fix
Keep filter() predicates stateless and non-interfering. If you need to collect additional information, use collect() with a custom collector instead of side-effecting in filter().
×

Using filter().findFirst() when anyMatch() suffices

Symptom
The code filter().findFirst().isPresent() works but is verbose and creates an Optional overhead. Also, findFirst() returns the first element even if you only care about existence.
Fix
Use anyMatch(predicate) directly — it's cleaner and short-circuits as soon as it finds a match, without needing to extract an element.
×

Assuming filter() on parallel stream is faster without checking predicate cost

Symptom
Parallel stream performs worse than sequential on small datasets or when predicate is cheap (e.g., simple integer comparison).
Fix
Only use parallelStream() when the dataset is large (tens of thousands+) and the predicate is CPU-intensive (e.g., complex regex, database call). For most cases, sequential is fine.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between Stream.filter() and Stream.anyMatch()?
Q02JUNIOR
How would you filter a List to only include completed payments ...
Q03SENIOR
Is Stream.filter() eager or lazy? What are the implications?
Q04SENIOR
How does multiple filter() calls affect performance compared to a single...
Q05SENIOR
Explain a production incident where filter() caused a failure and how yo...
Q01 of 05JUNIOR

What is the difference between Stream.filter() and Stream.anyMatch()?

ANSWER
filter() is an intermediate operation that returns a new stream containing only elements that satisfy the predicate. It's lazy and requires a terminal operation. anyMatch() is a short-circuiting terminal operation that returns a boolean indicating whether any element matches the predicate. Use filter() when you want a filtered collection; use anyMatch() when you only need to know if at least one element matches.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
How do I filter a list in Java 8+?
02
How do I filter null values from a Java stream?
03
Can I combine multiple filter conditions in one stream?
04
What's the difference between filter().findFirst() and anyMatch()?
05
Does filter() support parallel streams?
🔥

That's Collections. Mark it forged?

3 min read · try the examples if you haven't

Previous
HashMap vs Hashtable in Java
20 / 21 · Collections
Next
Java Map containsKey(): Check if a Key Exists