Back to Stream: filter
Q2Question 2 of 19

filter(): what Predicate it accepts, how it works internally (lazy, spliterator, predicate test)

Key Concepts
  • filter() accepts Predicate<? super T>, meaning it can take a predicate of the stream's element type or any supertype.
  • It returns a new Stream containing only elements where predicate.test(element) returns true.
  • Internally, filter is a lazy StatelessOp: calling it just wraps the upstream in a new pipeline stage with no execution.
  • The Spliterator drives element traversal: tryAdvance pulls one element at a time through the filter test.
  • Predicate supports composition with and(), or(), negate(), and the static Predicate.not() (Java 11).
  • filter is stateless: it does not buffer elements and works safely on infinite streams.
  • Named predicates stored as static fields or factory methods are reusable across pipelines and independently testable.

Definition

filter(Predicate) keeps elements where test() returns true, is a lazy stateless operation backed by the Spliterator engine, and supports Predicate composition with and/or/negate.
  • Signature: Stream filter(Predicate predicate) accepts a predicate of T or any supertype.
  • Behaviour: returns a new Stream containing only elements where predicate.test(element) returns true.
  • Lazy: calling filter just creates a StatelessOp pipeline stage. No element is tested until a terminal triggers.
  • Spliterator: the low-level engine that pulls elements one at a time. For each element, the predicate is tested.
  • Stateless: filter does not buffer elements and processes each independently with O(1) memory.
  • Composition: and(), or(), negate(), Predicate.not() create new predicates from existing ones.

filter() is the KYC verification clerk at National Bank of India sitting on the conveyor belt. Each customer application arrives one at a time (Spliterator pulls). The clerk checks "does this customer pass KYC?" (predicate.test). If YES, pass to the next station. If NO, reject and pull the next application. The clerk does not start until the manager (terminal) says begin.

Understanding the Concepts

How does filter work internally?

filter accepts a Predicate and returns a new Stream containing only matching elements. It is lazy, stateless, and processes one element at a time.

What Predicate does filter accept?

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;

public class FilterPredicateDemo {
    record Account(String id, BigDecimal balance, boolean active) {}

    public static void main(String[] args) {
        List<Account> accounts = List.of(
            new Account("SAV2001", new BigDecimal("125000"), true),
            new Account("SAV2002", new BigDecimal("8000"), false),
            new Account("CUR3001", new BigDecimal("250000"), true)
        );

        // Lambda predicate:
        accounts.stream().filter(a -> a.active()).toList();

        // Method reference:
        accounts.stream().filter(Account::active).toList();

        // Predicate<? super Account> accepts parent types:
        Predicate<Object> notNull = Objects::nonNull;
        accounts.stream().filter(notNull).toList(); // OK: Object is super of Account
    }
}

How does Predicate composition work?

JAVA
package com.bank.service;

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

public class PredicateComposition {
    record Account(String id, BigDecimal balance, boolean active) {}

    public static void main(String[] args) {
        List<Account> accounts = List.of(
            new Account("SAV2001", new BigDecimal("125000"), true),
            new Account("SAV2002", new BigDecimal("8000"), false),
            new Account("CUR3001", new BigDecimal("250000"), true),
            new Account("SAV2003", new BigDecimal("50000"), true)
        );

        Predicate<Account> isActive = Account::active;
        Predicate<Account> isLarge = a -> a.balance().compareTo(new BigDecimal("100000")) > 0;

        // Composition:
        List<Account> activeLarge = accounts.stream()
            .filter(isActive.and(isLarge))   // AND
            .toList();

        List<Account> activeOrLarge = accounts.stream()
            .filter(isActive.or(isLarge))    // OR
            .toList();

        List<Account> inactive = accounts.stream()
            .filter(isActive.negate())        // NOT
            .toList();

        System.out.println("Active AND large: " + activeLarge.size());
        System.out.println("Active OR large: " + activeOrLarge.size());
        System.out.println("Inactive: " + inactive.size());
    }
}
TEXT
Active AND large: 2
Active OR large: 3
Inactive: 1

How does the Spliterator drive filter execution?

The Spliterator is the low-level engine that traverses stream elements. For sequential streams, tryAdvance(Consumer) pulls one element at a time. For each element, predicate.test(element) is called. If true, the element passes downstream. If false, it is dropped and the next element is pulled.

For parallel streams, trySplit() divides the source into partitions. Each partition's Spliterator independently tests elements against the predicate, and results are merged.

What is the difference between layered filters and composed predicates?

JAVA
// Layered: three separate filter calls
accounts.stream()
    .filter(Account::active)
    .filter(a -> a.balance().compareTo(new BigDecimal("100000")) > 0)
    .filter(a -> a.id().startsWith("SAV"))
    .toList();

// Composed: one filter call with composed predicate
Predicate<Account> combined = Account::active
    .and(a -> a.balance().compareTo(new BigDecimal("100000")) > 0)
    .and(a -> a.id().startsWith("SAV"));
accounts.stream().filter(combined).toList();

Both produce identical results because stream fusion processes each element through all stages in a single pass. The layered approach is better for debugging. The composed approach is better for unit testing the predicate in isolation.

What are common mistakes?

Forgetting that filter returns a new Stream and does not modify the original collection. Using filter for side effects (modifying external state inside the predicate). Not providing proper hashCode/equals when using distinct after filter.

What should a senior engineer know?

Named predicates stored as static factory methods in a FilterRules class are reusable across different streams and independently testable. Predicate.not() (Java 11) replaces the verbose Predicate.not(Account::isDormant) pattern. filter is the most commonly used intermediate operation and is the first thing to place in a pipeline to reduce the dataset early.

Visual Overview

DIAGRAM
  filter() internal flow:
  ─────────────────────────
  Terminal says: "give me an element"
       ↑
  filter stage: asks upstream for element
       ↑
  source yields element E
       │
  predicate.test(E)
       │
  ┌────▼────┐     ┌────────────┐
  │  true   │────→│ pass to    │
  │         │     │ downstream │
  └─────────┘     └────────────┘
  ┌────▼────┐
  │  false  │────→ drop, pull next
  └─────────┘

  Predicate composition:
  ─────────────────────
  isActive.and(isLarge)  → both must be true
  isActive.or(isLarge)   → either can be true
  isActive.negate()      → invert result
  Predicate.not(p)       → static NOT (Java 11)

Key Takeaway

filter accepts Predicate and returns a new Stream containing only elements where the predicate returns true. It is a lazy, stateless operation: calling it creates a pipeline stage with no execution. The Spliterator engine pulls elements one at a time when a terminal triggers, and each element is independently tested.

Predicate composition with and, or, and negate lets you build complex filters from simple reusable building blocks. Named predicates stored as static fields are testable in isolation and reusable across pipelines.

Layered filters (three filter calls) and composed predicates (one filter call with and/or) produce identical results due to stream fusion. The layered approach is better for debugging; the composed approach is better for unit testing.

Remember one sentence: filter keeps elements where the predicate returns true, is stateless with O(1) memory, and supports and/or/negate composition.

---

Share
Use ← → arrow keys to navigate