🌊 Раздел 8 · Вопрос #3

Что делает операция filter()

Она принимает функциональный интерфейс java.util.function.Predicate (метод boolean test(T t)): если предикат возвращает true, элемент пропускается дальше по конвейеру; если f...


🟢 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)

Каждая промежуточная операция модифицирует характеристики потока:

  1. Флаг SIZED — СБРАСЫВАЕТСЯ (NOT_SIZED): Компилятор и конвейер больше не знают точного количества элементов, которые выйдут из фильтра. Терминальные операции (например, toArray()) больше не могут выделить массив точного размера заранее.
  2. Флаги 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.


🎯 Шпаргалка для интервью

Вопросы для быстрой проверки

  1. Является ли операция filter() промежуточной или терминальной? Ленивая она или жадная? Ответ: filter() — это промежуточная (intermediate) ленивая (lazy) операция. Она не производит никаких вычислений над элементами источника до тех пор, пока на конвейере не будет вызвана терминальная операция.
  2. Как filter() влияет на характеристики стрима (SIZED, SORTED, DISTINCT)? Ответ: Характеристика SIZED сбрасывается (размер результирующего потока становится неизвестен). Однако характеристики SORTED (порядок сортировки) и DISTINCT (уникальность) гарантированно сохраняются, так как удаление элементов не нарушает порядок и не порождает дубликатов.
  3. Что делает реализация метода accept(T value) в интерфейсе Sink для операции filter()? Ответ: Она выполняет проверку if (predicate.test(value)) downstream.accept(value);. Если предикат истинен, элемент передается следующей стадии конвейера; если ложен — вызов завершается, и элемент отбрасывается.
  4. Что произойдет, если в бесконечном стриме 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”: Порядок элементов никогда не нарушается операцией фильтрации.

Связанные темы