Что делает операция filter()
Она принимает функциональный интерфейс java.util.function.Predicate
🟢 Junior Level
Краткий ответ для собеседования (30 секунд)
filter(Predicate<? super T> predicate) — это промежуточная (intermediate) ленивая операция Stream API, предназначенная для отбора элементов потока данных по заданному условию.
Она принимает функциональный интерфейс java.util.function.Predicate<T> (метод boolean test(T t)): если предикат возвращает true, элемент пропускается дальше по конвейеру; если false — элемент отбрасывается.
Операция является без сохранения состояния (Stateless): каждый элемент проверяется автономно, независимо от других, что делает операцию исключительно производительной и безопасной для параллельной обработки.
Базовые примеры на Java 21
import java.util.List;
import java.util.Objects;
public class FilterBasicsDemo {
record Employee(String name, int age, String department) {}
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Alice", 28, "IT"),
new Employee("Bob", 45, "HR"),
new Employee("Charlie", 32, "IT"),
new Employee("Diana", 19, "Sales")
);
// 1. Фильтрация по числовому и строковому условию
List<String> itSeniors = employees.stream()
.filter(e -> "IT".equals(e.department()))
.filter(e -> e.age() >= 30)
.map(Employee::name)
.toList();
System.out.println("IT сотрудники 30+: " + itSeniors); // [Charlie]
// 2. Идиоматическая фильтрация null-значений
List<String> rawData = List.of("java", null, "stream", null, "api");
List<String> cleanedData = rawData.stream()
.filter(Objects::nonNull) // Защита от NullPointerException
.map(String::toUpperCase)
.toList();
System.out.println("Cleaned: " + cleanedData); // [JAVA, STREAM, API]
}
}
🟡 Middle Level
Внутреннее устройство: Sink.ChainedReference в OpenJDK
Внутри библиотеки java.util.stream операция filter не создает промежуточных коллекций. Она оборачивает нижележащий обработчик конвейера в класс Sink.ChainedReference:
// Упрощенная реализация внутри OpenJDK (ReferencePipeline.java):
@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) {
// Если предикат истинен - проталкиваем элемент следующему этапу (downstream)
if (predicate.test(u)) {
downstream.accept(u);
}
// Если ложен - элемент просто игнорируется (отброшен)
}
};
}
Архитектурный принцип Early Filtering (Ранняя фильтрация)
Порядок стадий в конвейере критически влияет на производительность. Фильтрующие операции должны располагаться как можно ближе к источнику данных, до ресурсоемких трансформаций:
// ❌ ПЛОХО: тяжелый маппинг (парсинг JSON, криптография) выполняется для ВСЕХ 1 000 000 элементов!
stream.map(this::heavyCryptographicHash)
.filter(hash -> hash.startsWith("0000"))
.toList();
// ✅ ОПТИМАЛЬНО: сначала отсекаем 99% мусорных данных легким предикатом
stream.filter(this::isCandidate)
.map(this::heavyCryptographicHash)
.filter(hash -> hash.startsWith("0000"))
.toList();
Композиция предикатов: Несколько filter() vs Predicate.and()
В Java есть два пути объединения условий:
// Вариант 1: Цепочка независимых вызовов filter()
stream.filter(Objects::nonNull)
.filter(user -> user.age() > 18)
.filter(User::isActive);
// Вариант 2: Единый композитный предикат через функциональные методы
Predicate<User> validAdult = Objects::nonNull;
validAdult = validAdult.and(u -> u.age() > 18).and(User::isActive);
stream.filter(validAdult);
Сравнение подходов:
- Цепочка
filter()(Вариант 1): Наиболее читаема, идиоматична для Java. Благодаря оптимизации Loop Fusion и JIT-инлайнингу компилятор C2 сжимает цепочку в единый машинный цикл без потери производительности. - Композиция
Predicate.and()(Вариант 2): Предпочтительна, когда предикаты формируются динамически (например, в конструкторах запросов или UI-фильтрах).
🔴 Senior Level
Влияние на флаги характеристик конвейера (Stream Flags)
Каждая промежуточная операция модифицирует характеристики потока:
- Флаг
SIZED— СБРАСЫВАЕТСЯ (NOT_SIZED): Компилятор и конвейер больше не знают точного количества элементов, которые выйдут из фильтра. Терминальные операции (например,toArray()) больше не могут выделить массив точного размера заранее. - Флаги
SORTEDиDISTINCT— СОХРАНЯЮТСЯ: Фильтрация удаляет элементы, но не меняет их относительный порядок и не создает дубликатов. Если исходный стрим был отсортирован (SORTED), конвейер послеfilter()сохраняет признак отсортированности, избавляя последующие стадии от повторной сортировки!
Симбиоз с Short-Circuiting операциями
В связке с операциями короткого замыкания (findFirst(), findAny(), limit()) операция filter не тестирует всю коллекцию:
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Integer firstEven = numbers.stream()
.filter(n -> {
System.out.println("Тестируем: " + n);
return n % 2 == 0;
})
.findFirst()
.orElseThrow();
Вывод в консоли:
Тестируем: 1
Тестируем: 2
Как только элемент 2 прошел предикат, операция findFirst() сигнализирует cancellationRequested() == true, и проверка элементов 3..10 даже не начинается.
Архитектурный антипаттерн: In-Memory Database Filtering
Одной из самых разрушительных практик в микросервисах является замена фильтрации базы данных на фильтрацию в Java:
// ❌ КАТАСТРОФА ДЛЯ ПРОИЗВОДИТЕЛЬНОСТИ:
// Выгружаем 500 000 строк из PostgreSQL в память Java-сервиса через Hibernate...
List<Order> orders = orderRepository.findAll().stream()
.filter(o -> o.getStatus() == OrderStatus.NEW)
.toList(); // OutOfMemoryError, гигантский трафик БД, сетевой затор
// ✅ ПРАВИЛЬНО: Перенос фильтрации на уровень СУБД (SQL Pushdown)
List<Order> orders = orderRepository.findAllByStatus(OrderStatus.NEW);
Фильтрация в Stream API предназначена для работы с локальными коллекциями в памяти или потоками вычислений, но не должна подменять индексированные SQL-запросы WHERE.
🎯 Шпаргалка для интервью
Вопросы для быстрой проверки
- Является ли операция
filter()промежуточной или терминальной? Ленивая она или жадная? Ответ:filter()— это промежуточная (intermediate) ленивая (lazy) операция. Она не производит никаких вычислений над элементами источника до тех пор, пока на конвейере не будет вызвана терминальная операция. - Как
filter()влияет на характеристики стрима (SIZED,SORTED,DISTINCT)? Ответ: ХарактеристикаSIZEDсбрасывается (размер результирующего потока становится неизвестен). Однако характеристикиSORTED(порядок сортировки) иDISTINCT(уникальность) гарантированно сохраняются, так как удаление элементов не нарушает порядок и не порождает дубликатов. - Что делает реализация метода
accept(T value)в интерфейсеSinkдля операцииfilter()? Ответ: Она выполняет проверкуif (predicate.test(value)) downstream.accept(value);. Если предикат истинен, элемент передается следующей стадии конвейера; если ложен — вызов завершается, и элемент отбрасывается. - Что произойдет, если в бесконечном стриме
Stream.iterate(0, i -> i + 1)вызватьfilter(i -> i < 0).findFirst()? Ответ: Метод зависнет в бесконечном цикле. Так как в стриме генерируются только неотрицательные числа ($0, 1, 2 \dots$), предикатi < 0никогда не вернетtrue, и конвейер будет бесконечно потреблять CPU в тщетных поисках первого совпадения.
Типичные ошибки и Red Flags
- ❌ Утверждение: “Метод
filter()сразу создает отфильтрованный список элементов”: Метод ленив и вычисляет элементы потоково по одному за раз. - ❌ Мутация внешних переменных внутри предиката: Нарушает принцип чистоты функций и приводит к состояниям гонки в параллельных стримах.
- ❌ Фильтрация сотен тысяч сущностей в Java вместо SQL-запроса
WHERE: Приводит к исчерпанию кучи (OutOfMemoryError) и деградации сети. - ❌ Утверждение: “
filter()сбрасывает порядок сортировкиSORTED”: Порядок элементов никогда не нарушается операцией фильтрации.