Back to Blog
Java

Java Stream filter(): Usage, Predicates, and Performance

Learn how to use Java Stream filter() with Predicate, combine conditions, avoid common mistakes, and understand important performance tradeoffs.

Java StreamsStream APIPredicateFunctional ProgrammingJava 8
A Java stream pipeline with a filter operation selecting elements that match a predicate.

The filter() method is an intermediate operation on Stream<T>. It accepts a Predicate<T> and returns a new stream containing only the elements for which the predicate returns true. This allows you to express conditional selection declaratively and chain it with other stream operations such as map(), sorted(), and limit().

Basic Usage of filter()

The simplest filter() call applies a lambda to each element. Elements that evaluate the predicate to true are kept; elements that evaluate to false are dropped:

import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David"); List<String> longNames = names.stream() .filter(name -> name.length() > 4) .collect(Collectors.toList());

The resulting list contains Alice and Charlie. The original names list is unchanged; filter() does not modify the stream source.

filter() is lazy. The predicate is not evaluated until a terminal operation such as collect() or forEach() is invoked. This laziness is what makes short-circuiting pipelines possible, for example filter(...).limit(5).

Combining Multiple Conditions with Predicate

A single filter() call accepts one Predicate, but you can combine conditions using the default methods on Predicate: and(), or(), and negate().

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Predicate<Integer> isEven = n -> n % 2 == 0; Predicate<Integer> isGreaterThanFour = n -> n > 4; List<Integer> result = numbers.stream() .filter(isEven.and(isGreaterThanFour)) .collect(Collectors.toList());

This returns [6, 8, 10]. Use or() to include elements that match any of the conditions, and negate() to invert a predicate. For complex conditions, store predicates in named variables or methods to keep the pipeline readable.

Filtering Null Values and Working with Optional

A predicate that calls a method or accesses a field on a possibly null value can throw a NullPointerException. You can remove nulls first with Objects::nonNull:

List<String> listWithNulls = Arrays.asList("one", null, "two", null); List<String> nonNull = listWithNulls.stream() .filter(Objects::nonNull) .collect(Collectors.toList());

When a stream contains Optional values from lookups that may not find a result, one approach is to keep present values and then unwrap them:

Stream<Optional<String>> streamOfOptionals = ...; List<String> filtered = streamOfOptionals .filter(Optional::isPresent) .map(Optional::get) .filter(s -> s.startsWith("A")) .collect(Collectors.toList());

In Java 9 and later, flatMap(Optional::stream) makes the same pattern more concise:

List<String> filtered = streamOfOptionals .flatMap(Optional::stream) .filter(s -> s.startsWith("A")) .collect(Collectors.toList());

Performance Considerations

filter() is not inherently faster than a for loop. It introduces some stream infrastructure overhead and lambda dispatch. For ordinary in-memory collections that overhead is usually small, and the main benefits are readability, composability, and lazy evaluation.

If a pipeline stops early, streams can avoid processing the entire source:

List<Integer> largeList = ...; List<Integer> firstFive = largeList.stream() .filter(n -> n % 3 == 0) .limit(5) .collect(Collectors.toList());

In a sequential stream, this stops after five matches have been found. A hand-written loop can also break early, but the stream form makes the short-circuit explicit.

If you collect all filtered values, the stream will visit every element, similar to a loop with a conditional check. Parallel streams can improve throughput on large datasets, but they add coordination overhead and are not a default optimization. Keep predicates stateless and non-interfering if you use parallel streams.

Common Mistakes and Pitfalls

A stream can be consumed only once. After a terminal operation such as collect() or forEach(), trying to call another stream method on the same stream throws IllegalStateException. Create a new stream from the source for each pipeline.

Do not modify the source collection while streaming. Removing elements from the underlying collection during stream processing can cause ConcurrentModificationException or undefined behavior. If you need to conditionally remove elements from a Collection, use removeIf():

names.removeIf(name -> name.length() <= 4);

Avoid stateful predicates. A predicate that depends on mutable state, such as a counter or a shared flag, can produce inconsistent results in sequential and especially parallel streams. Prefer predicates that depend only on the element being tested.

When to Use filter() vs Alternatives

Use Collection.removeIf() when you want to mutate the collection directly and no further stream operations are needed. removeIf() returns boolean, indicating whether any element was removed.

Use filter() when you want to keep the original collection unchanged, or when the filter is part of a longer pipeline with map(), sorted(), limit(), or a terminal operation other than collect().

A traditional for loop is still reasonable when you need indexed access or need to break out of a complex loop based on conditions that are easier to express imperatively.

Advanced Filtering with Custom Predicates

You can reuse predicates by defining them as static methods and referring to them with method references:

public class NameFilters { public static boolean isLongName(String name) { return name.length() > 4; } } List<String> longNames = names.stream() .filter(NameFilters::isLongName) .collect(Collectors.toList());

Predicates can also be composed at runtime. For example, you can build a filter condition based on user input:

Predicate<String> predicate = name -> name.startsWith("A"); if (includeLongNames) { predicate = predicate.or(NameFilters::isLongName); }

Here, includeLongNames is a boolean flag set from application logic.

Edge Cases and Parallel Streams

For parallel streams, keep predicates stateless and non-interfering. Stateful predicates can produce inconsistent results. If a predicate throws a runtime exception, the exception is generally propagated to the caller through the terminal operation, but parallel execution can make the timing and which element caused it less predictable.

Encounter order matters for terminal operations such as collect(Collectors.toList()) or findFirst(). On a sequential stream with an ordered source, those operations observe elements in encounter order. Parallel streams can also respect encounter order for many ordered pipelines, but forEach() does not guarantee it; use forEachOrdered() when the terminal operation must visit elements in source order. If order matters and you do not need parallel execution, stay with a sequential stream.

filter() is a fundamental building block of the Java Stream API. Knowing how to write clear predicates, compose conditions, and choose between streams and direct collection operations will make your data-processing code easier to read and maintain.

Java Stream filter(): Practical Usage and Code Examples | RYUSLOG DEV