Java Stream filter() Method: Filter Stream Elements with Practical Examples

0

The filter() method is one of the most useful intermediate operations in the Java Stream API. It allows you to keep only the elements that satisfy a particular condition while discarding the rest.


If you have ever written a loop that says, "for each item, check this condition and keep it if it matches," you have already understood the basic idea behind filter(). The Stream API simply expresses that intention in a more declarative form.

Why Do We Need filter()?

Suppose an application contains a list of employee salaries and you need only the salaries above ₹50,000. A traditional loop works perfectly well, but you have to manually manage iteration, conditions, and result storage.

import java.util.ArrayList;
import java.util.List;

public class TraditionalFiltering {
    public static void main(String[] args) {

        List<Integer> salaries = List.of(
            35000, 52000, 68000, 42000, 90000
        );

        List<Integer> result = new ArrayList<>();

        for (Integer salary : salaries) {
            if (salary > 50000) {
                result.add(salary);
            }
        }

        System.out.println(result);
    }
}

With filter(), the same intention can be expressed directly:

import java.util.List;

public class StreamFiltering {
    public static void main(String[] args) {

        List<Integer> salaries = List.of(
            35000, 52000, 68000, 42000, 90000
        );

        List<Integer> result = salaries.stream()
                                       .filter(salary -> salary > 50000)
                                       .toList();

        System.out.println(result);
    }
}

The stream version focuses on the business rule: keep salaries greater than 50,000. That is the central strength of filter().

Important: filter() does not modify the original source. It creates a stream containing only the elements that satisfy the supplied condition.

Syntax of filter()

The basic syntax is:

stream.filter(predicate)

The method accepts a Predicate<T>. A predicate represents a condition that accepts an element and returns either true or false.

Predicate<Integer> condition =
    number -> number > 50;

When the predicate returns true, the element remains in the stream. When it returns false, the element is discarded from that pipeline.

Predicate Result What Happens to the Element?
true Element continues through the stream
false Element is removed from that stream pipeline

Filtering Numbers

The simplest examples involve numeric conditions.

import java.util.List;

public class NumberFilter {
    public static void main(String[] args) {

        List<Integer> numbers = List.of(
            5, 10, 15, 20, 25, 30
        );

        numbers.stream()
               .filter(number -> number > 15)
               .forEach(System.out::println);
    }
}

Only 20, 25, and 30 satisfy the condition, so those values continue to the terminal operation.

Filtering Even Numbers

The condition can be more specific. For example, you can keep only even numbers by checking the remainder after division by two.

import java.util.List;

public class EvenNumberFilter {
    public static void main(String[] args) {

        List<Integer> numbers = List.of(
            1, 2, 3, 4, 5, 6, 7, 8
        );

        numbers.stream()
               .filter(number -> number % 2 == 0)
               .forEach(System.out::println);
    }
}

The predicate returns true only when the number is divisible by two without a remainder.

Filtering Strings

The filter() method becomes particularly useful when working with strings.

import java.util.List;

public class StringFilter {
    public static void main(String[] args) {

        List<String> names = List.of(
            "Amit",
            "Anita",
            "Rahul",
            "Arjun",
            "Neha"
        );

        names.stream()
             .filter(name -> name.startsWith("A"))
             .forEach(System.out::println);
    }
}

Only names beginning with A remain in the pipeline.

You can filter according to length as well:

names.stream()
     .filter(name -> name.length() > 4)
     .forEach(System.out::println);

Filtering with Multiple Conditions

Real business rules often contain more than one condition. You can combine conditions inside the predicate using logical operators such as && and ||.

import java.util.List;

public class MultipleConditions {
    public static void main(String[] args) {

        List<Integer> numbers = List.of(
            10, 15, 20, 25, 30, 35, 40
        );

        numbers.stream()
               .filter(number -> number > 15 && number < 35)
               .forEach(System.out::println);
    }
}

The condition requires the number to be greater than 15 and less than 35. Therefore, 20, 25, and 30 pass the filter.

Using OR Conditions

The logical OR operator allows an element to pass when at least one condition is true.

numbers.stream()
       .filter(number -> number < 10 || number > 30)
       .forEach(System.out::println);

Here, a number is retained when it is either less than ten or greater than thirty.

Using Multiple filter() Calls

Instead of placing every condition inside one large lambda expression, you can use multiple filter() operations.

numbers.stream()
       .filter(number -> number > 10)
       .filter(number -> number % 2 == 0)
       .forEach(System.out::println);

The first filter keeps numbers greater than ten. The second filter keeps only the even numbers among those remaining.

This can make complex rules easier to read because each filtering stage has a single responsibility.

Instructor Tip: If combining many conditions makes a lambda difficult to understand, split the logic into multiple filters or move the condition into a well-named method.

filter() Is Lazy

Like other intermediate operations, filter() is lazy. The predicate is not normally evaluated simply because filter() appears in the pipeline.

import java.util.List;

public class LazyFilter {
    public static void main(String[] args) {

        List<Integer> numbers = List.of(10, 20, 30);

        numbers.stream()
               .filter(number -> {
                   System.out.println("Testing " + number);
                   return number > 15;
               });

        System.out.println("Pipeline created");
    }
}

The predicate does not execute because there is no terminal operation. Add forEach(), count(), toList(), or another terminal operation, and the filtering stage becomes active.

filter() with collect()

A common pattern is to filter elements and then collect the survivors into a collection.

import java.util.List;

public class FilterToList {
    public static void main(String[] args) {

        List<String> languages = List.of(
            "Java",
            "Go",
            "Python",
            "C++",
            "JavaScript"
        );

        List<String> result = languages.stream()
                                       .filter(language -> language.length() > 3)
                                       .toList();

        System.out.println(result);
    }
}

The filter decides which elements survive, while toList() materializes those elements into a result list.

filter() with Objects

In professional applications, filtering is often applied to domain objects rather than primitive values.

import java.util.List;

public class EmployeeFilter {

    record Employee(
        String name,
        String department,
        double salary
    ) {}

    public static void main(String[] args) {

        List<Employee> employees = List.of(
            new Employee("Amit", "IT", 65000),
            new Employee("Priya", "HR", 52000),
            new Employee("Rahul", "IT", 78000),
            new Employee("Neha", "Finance", 71000)
        );

        employees.stream()
                 .filter(employee -> employee.salary() > 60000)
                 .forEach(System.out::println);
    }
}

The predicate examines the salary property of each employee. Only employees earning more than ₹60,000 continue through the pipeline.

Filtering by Multiple Object Properties

Business rules often combine several object properties.

employees.stream()
         .filter(employee ->
             employee.department().equals("IT")
             && employee.salary() > 60000
         )
         .forEach(System.out::println);

This filter keeps only employees who belong to the IT department and earn more than ₹60,000.

Extracting a Reusable Predicate

When a condition is reused or becomes complex, defining a named Predicate can make the code easier to understand.

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

public class PredicateFilter {

    public static void main(String[] args) {

        List<Integer> numbers = List.of(
            10, 20, 30, 40, 50
        );

        Predicate<Integer> greaterThanTwenty =
            number -> number > 20;

        numbers.stream()
               .filter(greaterThanTwenty)
               .forEach(System.out::println);
    }
}

This approach is especially useful when the same business rule is used in multiple stream pipelines.

Combining Predicates

The Predicate interface provides methods such as and(), or(), and negate() for composing conditions.

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

public class CombinedPredicate {
    public static void main(String[] args) {

        List<Integer> numbers = List.of(
            10, 20, 30, 40, 50
        );

        Predicate<Integer> greaterThanTen =
            number -> number > 10;

        Predicate<Integer> even =
            number -> number % 2 == 0;

        numbers.stream()
               .filter(greaterThanTen.and(even))
               .forEach(System.out::println);
    }
}

The combined predicate keeps numbers that are both greater than ten and even.

Filtering Null Values

If a collection may contain null values, calling methods on the elements inside the predicate can cause a NullPointerException.

import java.util.List;
import java.util.Objects;

public class NullFiltering {
    public static void main(String[] args) {

        List<String> names = List.of(
            "Amit",
            null,
            "Priya",
            null,
            "Rahul"
        );

        names.stream()
             .filter(Objects::nonNull)
             .forEach(System.out::println);
    }
}

Filtering out null values first is a useful technique when subsequent operations assume that every element is non-null.

Important: Do not blindly call methods on stream elements when null values are possible. Either establish a non-null invariant earlier or explicitly filter null values.

Filtering Strings Safely

After removing null values, string-specific conditions can safely be applied.

names.stream()
     .filter(Objects::nonNull)
     .filter(name -> name.length() > 4)
     .forEach(System.out::println);

This pipeline first removes null elements and then keeps only names longer than four characters.

filter() and Short-Circuiting

The filter() operation itself does not determine when the stream should stop. However, when it is combined with a short-circuiting terminal operation such as findFirst() or anyMatch(), the pipeline may stop processing as soon as the terminal operation has enough information.

import java.util.List;

public class FilterShortCircuit {
    public static void main(String[] args) {

        List<Integer> numbers = List.of(
            5, 10, 15, 20, 25, 30
        );

        numbers.stream()
               .filter(number -> number % 5 == 0)
               .findFirst()
               .ifPresent(System.out::println);
    }
}

The pipeline does not need to find every matching number when only the first matching element is required.

filter() Followed by map()

A very common stream pattern is to filter objects first and then transform the elements that remain.

import java.util.List;

public class FilterMapExample {

    record Employee(String name, double salary) {}

    public static void main(String[] args) {

        List<Employee> employees = List.of(
            new Employee("Amit", 45000),
            new Employee("Priya", 72000),
            new Employee("Rahul", 85000),
            new Employee("Neha", 52000)
        );

        employees.stream()
                 .filter(employee -> employee.salary() > 60000)
                 .map(Employee::name)
                 .forEach(System.out::println);
    }
}

The filter removes employees below the salary threshold. Only after that filtering step does map() convert the remaining employee objects into names.

Remember: When a transformation is expensive, filtering first can be beneficial if the filtering condition can eliminate many elements.

filter() vs removeIf()

Both filter() and removeIf() can appear to solve a similar problem, but they have different purposes.

Feature filter() removeIf()
Used with Streams Collection
Purpose Creates a stream containing matching elements Removes matching elements from a mutable collection
Modifies source? No Yes, when the collection supports the operation
Returns Stream boolean

Choose filter() when you want to build a stream-processing pipeline. Choose removeIf() when your intention is to modify a collection directly.

Common Beginner Mistakes

  • Forgetting that filter() is lazy: The predicate normally does not execute until a terminal operation consumes the stream.
  • Expecting filter() to modify the original collection: Filtering produces another stream; it does not remove elements from the source collection.
  • Calling methods on possible null values: Check or remove null elements before dereferencing them.
  • Writing one enormous predicate: Break complex conditions into meaningful predicates or multiple filtering stages.
  • Using filter() when matching is all you need: If the requirement is simply to know whether an element exists, anyMatch() may express the intent better.
  • Filtering after expensive transformations unnecessarily: When possible, eliminate irrelevant elements before costly processing.

Best Practices

  • Keep filter predicates simple and focused.
  • Use descriptive methods for complex business conditions.
  • Filter invalid or irrelevant data before expensive transformations when the logic allows it.
  • Use multiple filter stages when they make the business rule easier to read.
  • Avoid modifying external state from inside a filter predicate.
  • Use Objects::nonNull when explicitly removing null elements from a stream.
  • Remember that filter() is for selecting stream elements, not directly modifying the source collection.

Interview Insights

Question: What does filter() do in Java Streams?

Answer: It selects elements that satisfy a supplied Predicate and returns a stream containing those elements.

Question: Is filter() an intermediate or terminal operation?

Answer: filter() is an intermediate operation because it returns another stream and can be followed by additional stream operations.

Question: Does filter() modify the original collection?

Answer: No. Filtering normally leaves the source collection unchanged.

Question: What functional interface does filter() accept?

Answer: It accepts a Predicate<T>, which evaluates each element and returns a boolean result.

Question: Can multiple filter() operations be chained?

Answer: Yes. Multiple filters can be chained when separating conditions improves readability or allows the pipeline to express distinct stages of business logic.

Quick Revision

Concept Key Point
filter() Selects elements that satisfy a condition
Type Intermediate operation
Argument Predicate<T>
true Element continues through the pipeline
false Element is excluded from that pipeline
Source Normally remains unchanged
Multiple filters Can be chained for clearer conditions
Null handling Use a null check or Objects::nonNull when necessary
Performance Filtering early can reduce unnecessary later processing

Final Takeaway

The filter() operation gives Java streams a clean way to express selection rules. It evaluates each element through a predicate and allows only matching elements to continue through the pipeline. Whether you are selecting active users, high-value orders, valid records, or numbers that satisfy a mathematical condition, filter() keeps the business rule close to the data-processing logic. Mastering it is one of the first steps toward writing stream pipelines that are concise, expressive, and easy to maintain.

Post a Comment

0Comments
Post a Comment (0)