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

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

Принимает Predicate — функциональный интерфейс с методом boolean test(T t).

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

filter(Predicate) — это промежуточная операция, которая оставляет только те элементы, которые удовлетворяют условию.

Почему лучше if внутри цикла: filter можно комбинировать в цепочки, менять порядок операций и легко параллелить без изменения логики. if внутри цикла — жёстко зашит в императивный стиль.

Принимает Predicate<T> — функциональный интерфейс с методом boolean test(T t).

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Оставляем только чётные числа
List<Integer> even = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());
// Результат: [2, 4, 6, 8, 10]

// Фильтрация null-элементов
list.stream()
    .filter(Objects::nonNull)
    .forEach(System.out::println);

Важно: filter сам по себе не запускает обработку — нужна terminal операция.

🟡 Middle Level

Внутренняя реализация

filter создает Sink.ChainedReference, метод accept которого:

Sink.ChainedReference — внутренний класс, который оборачивает следующий Sink в цепочке и вызывает его accept() только если Predicate возвращает true.

public void accept(T t) {
    if (predicate.test(t)) {
        downstream.accept(t); // передаём дальше
    }
    // иначе — элемент "проглатывается"
}

Stateless природа

Фильтр не зависит от других элементов — каждый обрабатывается независимо. Это делает filter идеальным для parallelStream.

Ранняя фильтрация (Early Filtering)

Чем раньше отсечете лишние данные, тем меньше работы у последующих дорогих операций:

// ХОРОШО — фильтр в начале
stream.filter(Objects::nonNull)
      .filter(s -> s.startsWith("A"))
      .map(expensiveMapping)

// ПЛОХО — expensiveMapping вызовется для всех элементов
stream.map(expensiveMapping)
      .filter(Objects::nonNull)

Цепочки фильтров vs сложный предикат

// Вариант А — читаемее
stream.filter(Objects::nonNull)
      .filter(s -> s.startsWith("A"))

// Вариант Б — чуть быстрее
stream.filter(s -> s != null && s.startsWith("A"))

Для большинства случаев приоритетнее читаемость (Вариант А).

🔴 Senior Level

Predicate Complexity и производительность

Если предикат выполняет тяжелую логику (регулярки, вызов внешнего кеша), он будет вызван для каждого элемента. Кэшируйте результаты тяжелых вычислений.

Short-circuiting взаимодействие

filter работает в паре с операциями короткого замыкания (findFirst, limit). Если фильтр нашел подходящий элемент, конвейер остановится — остальные элементы не обрабатываются.

Null-checks как стандарт де-факто

filter(Objects::nonNull) — стандартный паттерн для очистки данных перед обработкой, предотвращает NPE в последующих map.

Edge Cases

  • Side Effects в предикате: Никогда не меняйте внешние переменные внутри filter — это нарушает функциональную парадигму и приведет к багам в параллельных стримах
  • Optional.filter: У класса Optional тоже есть filter(), работает аналогично — превращает заполненный Optional в пустой при несовпадении условия

Диагностика

Filtering Metrics: Если фильтр отсеивает 99.9% данных в Java-коде — перенесите фильтрацию на уровень SQL-запроса.

Logging с peek():

stream.peek(e -> log.debug("Before: " + e))
      .filter(predicate)
      .peek(e -> log.debug("After: " + e))

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

Обязательно знать:

  • filter(Predicate) — промежуточная операция, оставляет элементы, удовлетворяющие условию
  • Stateless — каждый элемент обрабатывается независимо, идеально для parallelStream
  • Ранняя фильтрация: чем раньше filter, тем меньше работы у последующих дорогих операций
  • Работает с short-circuit операциями (findFirst, limit) — конвейер останавливается при нахождении
  • filter(Objects::nonNull) — стандартный паттерн для защиты от NPE
  • Фильтр сам не запускает обработку — нужна terminal операция

Частые уточняющие вопросы:

  • filter vs if внутри цикла? — filter декларативен, легко комбинировать и параллелить
  • Цепочка фильтров или один сложный предикат? — Цепочка читаемее, один предикат чуть быстрее
  • Что если предикат тяжёлый? — Кэшируйте результаты, вызывается для каждого элемента
  • Можно ли менять внешние переменные в предикате? — Нет, это нарушает функциональную парадигму

Красные флаги (НЕ говорить):

  • «filter сразу обрабатывает все элементы» — нет, ленивый до terminal операции
  • «Side effects в предикате — это нормально» — нет, приводит к багам в параллельных стримах
  • «filter сортирует элементы» — нет, только отфильтровывает, порядок сохраняется
  • «Если фильтр отсеивает 99% — это ок в Java-коде» — лучше перенести на уровень SQL

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

  • [[2. В чём разница между intermediate и terminal операциями]]
  • [[4. Что делает операция map()]]
  • [[9. Что такое параллельные стримы]]
  • [[5. Что делает операция collect()]]