🌊 Section 8 · Question #3

What does filter() operation do

It accepts a java.util.function.Predicate (whose single abstract method is boolean test(T t)). For each element in the stream, if the predicate evaluates to true, the element...


🟢 Junior Level

Short Interview Answer (30 seconds)

filter(Predicate<? super T> predicate) is an intermediate, lazy, and stateless operation in the Java Stream API that selects elements from a stream based on a boolean condition.

It accepts a java.util.function.Predicate<T> (whose single abstract method is boolean test(T t)). For each element in the stream, if the predicate evaluates to true, the element passes downstream to the next pipeline stage; if false, the element is immediately discarded.

Because filter() is stateless, each element is evaluated autonomously without requiring knowledge of previous elements. This allows the operation to run with minimal memory overhead and scale seamlessly across multi-threaded parallel streams.


Canonical Java 21 Examples

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

public class FilterBasicsDemo {

    public record Employee(String name, int age, String department) {}

    public static void main(String[] args) {
        List<Employee> employees = List.of(
            new Employee("Alice", 28, "Engineering"),
            new Employee("Bob", 45, "HR"),
            new Employee("Charlie", 32, "Engineering"),
            new Employee("Diana", 19, "Sales")
        );

        // 1. Filtering by multiple business predicates
        List<String> seniorEngineers = employees.stream()
            .filter(e -> "Engineering".equals(e.department()))
            .filter(e -> e.age() >= 30)
            .map(Employee::name)
            .toList();

        System.out.println("Senior Engineers: " + seniorEngineers); // [Charlie]

        // 2. Idiomatic Null-Safety Filtering
        List<String> rawTokens = List.of("jwt", null, "bearer", null, "oauth2");
        List<String> sanitizedTokens = rawTokens.stream()
            .filter(Objects::nonNull) // Shields downstream operations from NullPointerException
            .map(String::toUpperCase)
            .toList();

        System.out.println("Sanitized: " + sanitizedTokens); // [JWT, BEARER, OAUTH2]
    }
}

🟡 Middle Level

Under the Hood: Sink.ChainedReference in OpenJDK

Internally within java.util.stream.ReferencePipeline, filter() does not allocate intermediate collections. Instead, it wraps the downstream pipeline stage in a specialized Sink.ChainedReference:

// Simplified OpenJDK ReferencePipeline.java logic:
@Override
public Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> downstream) {
    return new Sink.ChainedReference<P_OUT, P_OUT>(downstream) {
        @Override
        public void accept(P_OUT u) {
            // If predicate evaluates to true, push element forward to the next stage
            if (predicate.test(u)) {
                downstream.accept(u);
            }
            // If false, the element is discarded immediately (no downstream call)
        }
    };
}

During loop execution, this nested accept() method is invoked for each element. The HotSpot C2 compiler aggressively inlines predicate.test() and downstream.accept(), fusing the filtering logic directly into the main loop.


The Early Filtering Principle (Pushdown Optimization)

The sequential placement of filter() in a pipeline dramatically impacts throughput. Filtering operations should always be positioned as early as possible in the stream pipeline to avoid expending CPU cycles on elements that will ultimately be discarded:

// ❌ INEFFICIENT: Executes heavy parsing/decryption on 1,000,000 records before discarding 990,000!
rawLogs.stream()
    .map(this::heavyCryptographicDecrypt) 
    .filter(log -> log.level().equals("CRITICAL"))
    .toList();

// ✅ OPTIMAL: Pre-filters raw strings with lightweight checks before heavy transformations
rawLogs.stream()
    .filter(raw -> raw.contains("CRITICAL")) // Fast byte/substring check
    .map(this::heavyCryptographicDecrypt)
    .toList();

Predicate Composition: Multiple filter() vs. Predicate.and()

Java developers can combine multiple conditions using two idiomatic patterns:

// Pattern 1: Chained filter() stages
stream.filter(Objects::nonNull)
      .filter(u -> u.age() > 18)
      .filter(User::isActive);

// Pattern 2: Composite Predicate using functional combinators
Predicate<User> validUser = Objects::nonNull;
validUser = validUser.and(u -> u.age() > 18).and(User::isActive);

stream.filter(validUser);

Comparison:

  • Chained filter() calls (Pattern 1): Highly readable and standard in modern Java. Due to JIT Loop Fusion, chaining multiple filter() calls incurs zero performance penalty compared to a single composite predicate.
  • Composite Predicate.and() (Pattern 2): Best suited for dynamic query builders where filtering criteria are assembled conditionally at runtime based on user input.

🔴 Senior Level

Stream Characteristics: What Happens to Stream Flags?

Every stream source possesses characteristic bitmask flags (SIZED, SORTED, DISTINCT, ORDERED). When an intermediate filter() stage is attached:

  1. The SIZED Flag is CLEARED (NOT_SIZED): Because the pipeline cannot anticipate how many elements will satisfy the predicate, the exact count becomes unknown. Consequently, downstream terminal operations (like toArray()) cannot pre-allocate an exact-sized array and must dynamically grow buffers.
  2. The SORTED Flag is PRESERVED: Removing elements from an ordered sequence never disrupts their relative sorted order. Subsequent stages (such as sorted()) detect that the stream is already SORTED and skip redundant sorting routines!
  3. The DISTINCT Flag is PRESERVED: If the upstream source contained no duplicate elements, filtering cannot introduce duplicates.

Short-Circuiting Synergy

When paired with short-circuiting operations (findFirst(), findAny(), limit()), filter() stops element evaluation immediately once a match satisfies the condition:

List<Integer> numbers = List.of(1, 3, 5, 8, 9, 11);

Integer firstEven = numbers.stream()
    .filter(n -> {
        System.out.println("Inspecting: " + n);
        return n % 2 == 0;
    })
    .findFirst()
    .orElseThrow();

Console Output:

Inspecting: 1
Inspecting: 3
Inspecting: 5
Inspecting: 8

As soon as element 8 satisfies the predicate, findFirst() signals cancellationRequested() == true. Evaluation halts, and remaining elements (9 and 11) are never touched.


Architectural Anti-Pattern: In-Memory Database Filtering

One of the most severe performance blunders in enterprise backend engineering is performing database filtering inside Java streams:

// ❌ ARCHITECTURAL DISASTER:
// Fetches 500,000 rows from PostgreSQL over JDBC into JVM heap...
List<Order> orders = orderRepository.findAll().stream()
    .filter(o -> o.getStatus() == OrderStatus.PENDING)
    .toList(); // High network latency, JDBC connection blocking, and JVM OutOfMemoryError!

// ✅ PRODUCTION STANDARD: Pushdown filtering to database indexes (SQL WHERE)
List<Order> orders = orderRepository.findAllByStatus(OrderStatus.PENDING);

Stream filtering is designed for in-memory collections and dynamic compute streams; it must never substitute for indexed database WHERE queries.


4 Tricky Questions

1. How does filter() alter stream characteristics (SIZED, SORTED, DISTINCT)?

Answer: filter() clears the SIZED characteristic (the stream becomes NOT_SIZED because the number of matching elements is unpredictable).

However, it preserves both SORTED and DISTINCT characteristics. Filtering elements out of a sorted set does not alter the relative order of the remaining elements, nor does it create duplicate values.


2. What happens if you execute Stream.iterate(0, i -> i + 1).filter(i -> i < 0).findFirst()?

Answer: It hangs indefinitely in an infinite loop, consuming 100% of a CPU core. Stream.iterate(0, i -> i + 1) generates the infinite sequence of non-negative integers ($0, 1, 2, 3 \dots$). Because the predicate i < 0 never evaluates to true, filter() never pushes an element downstream to findFirst(). findFirst() never receives an element to trigger short-circuit cancellation, so the stream generates numbers forever.


3. Can the Predicate passed to filter() declare or throw checked exceptions?

Answer: No. The functional interface java.util.function.Predicate<T> declares boolean test(T t) without a throws clause.

If a filtering operation throws a checked exception (e.g., IOException), the code will not compile. To handle checked exceptions, developers must either:

  1. Wrap the checked exception in a local try-catch inside the lambda and throw an unchecked RuntimeException or UncheckedIOException.
  2. Use an exception-handling adapter function (e.g., Sneaky Throws or Vavr’s CheckedFunction).

4. What happens if you mutate an external collection inside a filter() predicate in a parallel stream?

Answer: It violates functional non-interference and triggers race conditions. Predicates must be strictly stateless and side-effect-free. If multiple worker threads in .parallelStream() invoke filter(item -> sharedList.add(item)), concurrent unsynchronized writes to sharedList will cause lost updates, internal array corruption, and ArrayIndexOutOfBoundsExceptions.


🎯 Interview Cheat Sheet

Core Concepts Matrix

┌───────────────────────────┬────────────────────────────────────────────────────────────────────────┐
│ Dimension                 │ filter() Specification                                                 │
├───────────────────────────┼────────────────────────────────────────────────────────────────────────┤
│ Operation Type            │ Intermediate, Stateless, Lazy                                          │
│ Functional Argument       │ Predicate<? super T> (boolean test(T t))                               │
│ Characteristic Flags      │ Clears SIZED; Preserves SORTED and DISTINCT                            │
│ Memory Model              │ Zero intermediate collections; pushes via Sink.ChainedReference        │
│ Short-Circuiting          │ Cooperates with findFirst()/limit() via Sink.cancellationRequested()   │
│ Optimization Rule         │ Early Filtering: filter before expensive transformations               │
│ DB Integration            │ Never substitute for SQL WHERE clauses on persistent repositories      │
└───────────────────────────┴────────────────────────────────────────────────────────────────────────┘

Key Takeaways

  1. filter() selectively passes elements downstream if predicate.test(element) returns true.
  2. It is purely lazy and stateless; elements are evaluated individually on demand during terminal execution.
  3. Filtering preserves SORTED and DISTINCT characteristics, but clears SIZED.
  4. Always filter early in the pipeline to avoid wasting CPU on objects destined for disposal.
  5. In enterprise applications, never use stream filtering as an alternative to database query indexing.

Red Flags to Avoid

  • ❌ Claiming that filter() buffers elements into an intermediate ArrayList.
  • ❌ Asserting that filter() invalidates the sorting order of a sorted stream.
  • ❌ Fetching large unindexed tables from a database and filtering them in Java memory.
  • ❌ Performing side-effects or mutating state inside a filter predicate.