Java Stream filter vs map: Key Differences
java stream filter vs map: Understand the difference between Java Stream filter and map, when to use each operation, and how they combine in functional pipelines.
When building a stream pipeline in Java, two intermediate operations appear constantly: filter and map. Both transform the data flow, but they answer different questions. The java stream filter vs map decision comes down to whether you need to select elements or change them. filter keeps or discards elements based on a predicate, while map converts each element into a new value. Understanding this distinction is essential for writing readable and efficient stream code.
The Role of filter in a Stream
filter is an intermediate operation that takes a Predicate<T> and returns a stream containing only the elements that match the predicate. The stream size can shrink because elements that fail the test are removed. For example:
List<String> names = List.of("Alice", "Bob", "Charlie", "David"); List<String> longNames = names.stream() .filter(name -> name.length() > 4) .collect(Collectors.toList()); // longNames contains "Alice", "Charlie", "David"
The predicate is applied to each element, and only those that return true continue down the pipeline. filter never changes the type of the elements; it only reduces the count.
The Role of map in a Stream
map is an intermediate operation that takes a Function<T, R> and returns a stream of type R. It applies the function to every element, producing a new stream where each input element is replaced by the function's result. The stream size remains the same, but the element type can change. For example:
List<String> names = List.of("Alice", "Bob", "Charlie"); List<Integer> nameLengths = names.stream() .map(String::length) .collect(Collectors.toList()); // nameLengths contains 5, 3, 7
Here each string is converted to its length. map is a transformation operation: it changes the value and possibly the type of each element.
Key Differences Between filter and map
The two operations serve different purposes in a stream pipeline. The table below summarizes the most significant differences:
| Aspect | filter | map |
|---|---|---|
| Input | Predicate<T> | Function<T, R> |
| Output type | Same as input (Stream<T>) | Different (Stream<R>) |
| Stream size | May decrease | Stays the same |
| Element values | Unchanged | Transformed |
| Typical use | Selecting items based on condition | Converting items to another form |
filter is a selection operation; it decides which elements survive. map is a conversion operation; it decides what each element becomes. They are not interchangeable, but they are often used together.
When to Use filter vs map
Use filter when you need to narrow a collection down to elements that satisfy a condition. For example, filtering out null values, selecting only active users, or picking numbers greater than a threshold. Use map when you need to convert each element into something else, such as extracting a field from an object, converting a string to an integer, or applying a calculation to each number.
The choice is driven by the desired outcome. If the goal is to reduce the number of elements, use filter. If the goal is to change the representation of every element, use map. If both are needed, chain them in a logical order.
Combining filter and map in a Pipeline
Stream pipelines often combine both operations. Typically you filter first to remove unwanted elements, then map the remaining elements. This avoids unnecessary transformations on values that will be discarded. For example:
List<Order> orders = getOrders(); List<String> customerEmails = orders.stream() .filter(order -> order.isPaid()) .map(Order::getCustomerEmail) .collect(Collectors.toList());
Here filter selects only paid orders, and map extracts the email address from each selected order. The order of operations matters for both clarity and efficiency. Filtering first reduces the number of elements that the map function must process.
Performance and Lazy Evaluation Considerations
Both filter and map are intermediate operations, meaning they are lazy. No work happens until a terminal operation like collect, forEach, or reduce is called. The stream implementation processes elements one by one, applying each operation in sequence. This allows short-circuiting operations like limit or findFirst to avoid processing the entire stream.
Because of laziness, the order of operations can affect how many elements are processed. Placing filter before map often reduces the number of transformations, but the actual impact depends on the stream source and the terminal operation. There is no general performance rule that applies to all cases; you should measure with realistic data if performance is a concern.
Common Mistakes and Edge Cases
One common mistake is using map when you really need filter. For example, trying to remove null values with map by returning a default value is not the same as filtering them out. Another mistake is assuming filter can change the element type. It cannot; it only passes the original element through.
Edge cases include handling empty streams and null elements. If a predicate throws an exception, it will propagate during the terminal operation. Similarly, a map function that returns null can cause a NullPointerException later in the pipeline if the next operation expects non-null values. Always consider how your predicate and function behave with edge inputs.
When using parallel streams, both operations must be stateless and non-interfering to produce correct results. A map function that modifies shared state can cause race conditions. A filter predicate that depends on external mutable state is also unsafe. Keep both operations pure for reliable parallel execution.