Definition
- Signature:
Streamaccepts a predicate of T or any supertype.filter(Predicate super T> predicate) - 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?
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?
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());
}
}
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?
// 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
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 super T> 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.
---